Configure TLS Termination¶
How to set up TLS/SSL encryption for Mailu using Traefik ingress controller.
Problem¶
You need to configure secure HTTPS access to Mailu webmail and admin interfaces, plus TLS-encrypted SMTP/IMAP/POP3 connections for mail clients.
Solution¶
The cdk8s-mailu library is designed for Traefik TLS termination. Traefik handles TLS certificates (Let’s Encrypt) and decrypts traffic, while Mailu components communicate in plaintext internally. This is the recommended production pattern for Kubernetes deployments.
Two approaches available:
Automated (recommended): Use cdk8s-mailu’s built-in Traefik ingress support
Manual: Create Traefik IngressRoute resources separately (for advanced customization)
Architecture Overview¶
TLS Termination Flow:
Client (TLS)
↓
Traefik IngressRoute (TLS termination with Let's Encrypt)
↓
Mailu Front Service (plaintext, port 80 for HTTP, 25/587/465/993/995 for mail)
↓
Backend Services (plaintext internal communication)
Why this design?:
Centralized certificate management (Traefik + cert-manager)
Automatic certificate renewal
No certificate distribution to mail pods
Standard Kubernetes ingress pattern
Prerequisites¶
Before configuring TLS, ensure you have:
Traefik ingress controller installed
cert-manager installed (for Let’s Encrypt certificates)
DNS records pointing to your cluster:
mail.example.com→ Cluster ingress IP
Cluster ingress accessible from internet (ports 80, 443, 25, 587, 465, 993, 995)
Built-in TLS Configuration¶
cdk8s-mailu automatically configures Mailu for Traefik TLS termination:
import { App } from 'cdk8s';
import { MailuChart } from 'cdk8s-mailu';
const app = new App();
new MailuChart(app, 'mailu', {
namespace: 'mailu',
domain: 'example.com',
hostnames: ['mail.example.com'], // Used for TLS certificate
subnet: '10.42.0.0/16',
// ... database, redis, secrets config ...
});
app.synth();
What this does automatically:
Sets
TLS_FLAVOR=notls(Traefik handles TLS, not Mailu)Mounts nginx-patch ConfigMap to Front container
Patches nginx to support mail protocol TLS ports (587, 465, 993, 995)
Configures plaintext internal communication between components
No additional TLS configuration needed in the CDK8S code!
Option 1: Automated Traefik Ingress (Recommended)¶
New in cdk8s-mailu: Built-in support for automatic ingress resource creation.
Add the ingress configuration to your MailuChart:
import { App } from 'cdk8s';
import { MailuChart } from 'cdk8s-mailu';
const app = new App();
new MailuChart(app, 'mailu', {
namespace: 'mailu',
domain: 'example.com',
hostnames: ['mail.example.com'],
subnet: '10.42.0.0/16',
// Database, redis, secrets config...
database: { /* ... */ },
redis: { /* ... */ },
secrets: { /* ... */ },
// Automated Traefik ingress (NEW!)
ingress: {
enabled: true,
type: 'traefik',
traefik: {
hostname: 'mail.example.com',
certIssuer: 'letsencrypt-cluster-issuer', // Your cert-manager ClusterIssuer
enableTcp: true, // Enable TCP routes for SMTP/IMAP/POP3
smtpConnectionLimit: 15, // Max concurrent SMTP connections per IP
},
},
});
app.synth();
What this creates automatically:
✅ Kubernetes Ingress with cert-manager annotation for HTTPS (admin UI, webmail)
✅ Traefik TLSOption with Mailu-compatible cipher suites
✅ Traefik IngressRouteTCP for SMTP (port 25) with rate limiting
✅ Traefik IngressRouteTCP for SMTPS (port 465) with TLS termination
✅ Traefik IngressRouteTCP for Submission (port 587) with TLS termination
✅ Traefik IngressRouteTCP for IMAPS (port 993) with TLS termination
✅ Traefik IngressRouteTCP for POP3S (port 995) with TLS termination
✅ Traefik IngressRouteTCP for IMAP (port 143, plaintext)
✅ Traefik IngressRouteTCP for POP3 (port 110, plaintext)
✅ Traefik MiddlewareTCP for SMTP connection rate limiting
Benefits:
One-click ingress setup
Type-safe configuration
Automatic service name resolution
Production-ready defaults
Consistent with Mailu security requirements
Configuration Options¶
All ingress options are optional with sensible defaults:
ingress: {
enabled: true, // Enable ingress creation
type: 'traefik', // Ingress type (only 'traefik' supported currently)
traefik: {
hostname: 'mail.example.com', // Required: FQDN for ingress
certIssuer: 'letsencrypt-cluster-issuer', // Default: 'letsencrypt-cluster-issuer'
enableTcp: true, // Default: true (enable TCP routes)
smtpConnectionLimit: 15, // Default: 15 (concurrent SMTP connections per IP)
},
}
After deploying, cert-manager will automatically provision a Let’s Encrypt certificate for mail.example.com.
Option 2: Manual Traefik IngressRoute Creation¶
If you need more control or want to customize the ingress resources, you can create them manually.
Step 1: Create TLS Certificate¶
Use cert-manager to provision a Let’s Encrypt certificate:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: mailu-tls
namespace: mailu
spec:
secretName: mailu-tls-cert
issuerRef:
name: letsencrypt-prod # Your ClusterIssuer
kind: ClusterIssuer
dnsNames:
- mail.example.com
Apply:
kubectl apply -f mailu-certificate.yaml
# Wait for certificate to be ready
kubectl get certificate -n mailu -w
Step 2: Create HTTP/HTTPS IngressRoute¶
Route web traffic (admin UI, webmail) through Traefik:
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mailu-web
namespace: mailu
spec:
entryPoints:
- websecure # HTTPS entry point
routes:
- match: Host(`mail.example.com`)
kind: Rule
services:
- name: mailu-front
port: 80 # Mailu front service (plaintext internal)
tls:
secretName: mailu-tls-cert # Certificate from cert-manager
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mailu-web-redirect
namespace: mailu
spec:
entryPoints:
- web # HTTP entry point
routes:
- match: Host(`mail.example.com`)
kind: Rule
middlewares:
- name: redirect-to-https
services:
- name: mailu-front
port: 80
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-to-https
namespace: mailu
spec:
redirectScheme:
scheme: https
permanent: true
Step 3: Create Mail Protocol IngressRoutes¶
Route SMTP, IMAP, POP3 traffic with TLS termination:
# SMTP Submission (port 587)
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: mailu-smtp-submission
namespace: mailu
spec:
entryPoints:
- smtp-submission # Traefik TCP entry point on port 587
routes:
- match: HostSNI(`*`)
services:
- name: mailu-front
port: 587
tls:
secretName: mailu-tls-cert
---
# SMTPS (port 465)
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: mailu-smtps
namespace: mailu
spec:
entryPoints:
- smtps # Traefik TCP entry point on port 465
routes:
- match: HostSNI(`*`)
services:
- name: mailu-front
port: 465
tls:
secretName: mailu-tls-cert
---
# IMAPS (port 993)
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: mailu-imaps
namespace: mailu
spec:
entryPoints:
- imaps # Traefik TCP entry point on port 993
routes:
- match: HostSNI(`*`)
services:
- name: mailu-front
port: 993
tls:
secretName: mailu-tls-cert
---
# POP3S (port 995)
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: mailu-pop3s
namespace: mailu
spec:
entryPoints:
- pop3s # Traefik TCP entry point on port 995
routes:
- match: HostSNI(`*`)
services:
- name: mailu-front
port: 995
tls:
secretName: mailu-tls-cert
---
# SMTP (port 25, plaintext for receiving mail from other servers)
# Port 25 routes directly to Postfix (bypassing Front/nginx) with rate limiting
apiVersion: traefik.io/v1alpha1
kind: MiddlewareTCP
metadata:
name: smtp-connection-limit
namespace: mailu
spec:
inFlightConn:
amount: 15 # Max 15 simultaneous connections per source IP
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: mailu-smtp
namespace: mailu
spec:
entryPoints:
- smtp # Traefik TCP entry point on port 25
routes:
- match: HostSNI(`*`)
middlewares:
- name: smtp-connection-limit # Apply rate limiting
services:
- name: mailu-postfix # Direct to Postfix (bypasses Front/nginx)
port: 25
# No TLS for port 25 (SMTP servers use STARTTLS opportunistically)
Apply:
kubectl apply -f mailu-ingressroutes.yaml
Step 4: Configure Traefik Entry Points¶
Ensure Traefik has the required TCP entry points configured. This is typically done in Traefik’s Helm values or static configuration:
# Traefik Helm values or static config
ports:
web:
port: 80
exposedPort: 80
websecure:
port: 443
exposedPort: 443
smtp:
port: 25
exposedPort: 25
smtp-submission:
port: 587
exposedPort: 587
smtps:
port: 465
exposedPort: 465
imaps:
port: 993
exposedPort: 993
pop3s:
port: 995
exposedPort: 995
SMTP Rate Limiting Strategy¶
Port 25 (SMTP) uses a hybrid rate limiting approach to protect against spam and connection flooding:
Layer 1: Traefik InFlightConn (Ingress Level)¶
The MiddlewareTCP resource limits simultaneous connections per source IP:
apiVersion: traefik.io/v1alpha1
kind: MiddlewareTCP
metadata:
name: smtp-connection-limit
namespace: mailu
spec:
inFlightConn:
amount: 15 # Max 15 concurrent connections per IP
What it protects against:
Connection flooding attacks
Resource exhaustion at ingress layer
Fast rejection before traffic reaches Postfix
Limitations:
Does NOT limit connection rate over time (e.g., rapid connect/disconnect)
Does NOT limit message rate or recipient rate
Layer 2: Postfix anvil (Application Level)¶
Postfix’s built-in anvil(8) daemon provides comprehensive SMTP rate limiting:
Configured automatically by cdk8s-mailu:
// In PostfixConstruct - automatically applied
POSTFIX_smtpd_client_connection_rate_limit: "60" // 60 connections per minute per IP
POSTFIX_smtpd_client_connection_count_limit: "10" // 10 simultaneous connections per IP
POSTFIX_smtpd_client_message_rate_limit: "100" // 100 messages per minute per IP
POSTFIX_smtpd_client_recipient_rate_limit: "300" // 300 recipients per minute per IP
POSTFIX_anvil_rate_time_unit: "60s" // Time unit for rate calculations
What it protects against:
High connection rates (rapid connect/disconnect attacks)
Message flooding
Recipient harvesting attacks
Spam relay attempts
How it works:
Postfix
anvildaemon tracks per-IP statistics in memoryAutomatically rejects connections/messages exceeding limits with SMTP error codes
Trusted networks (defined in
$mynetworks) are exempt from limitsStatistics reset on Postfix pod restart (no persistent state)
Why Port 25 Bypasses nginx¶
Traditional Mailu architecture:
Port 25: Traefik → Front (nginx) → Postfix
Optimized architecture (used by cdk8s-mailu):
Port 25: Traefik (InFlightConn) → Postfix (anvil rate limits)
Rationale:
Port 25 never requires authentication (RFC 5321 - MX mail delivery standard)
nginx provides zero security value for port 25 (no auth to proxy)
Postfix has robust spam filtering (Rspamd, DNSBL, rate limiting)
Reduced latency for incoming mail (one less proxy hop)
Improved reliability (nginx restart doesn’t affect MX delivery)
Authenticated ports (587, 465, 993, 995) still route through Front (nginx) for protocol-aware authentication proxy.
Adjusting Rate Limits¶
If you need to customize rate limits for your deployment size:
Small deployments (< 50 users):
Use default limits (60 conn/min, 100 msg/min)
Automated ingress:
smtpConnectionLimit: 15
Medium deployments (50-500 users):
Automated ingress:
smtpConnectionLimit: 25Increase Postfix limits:
connection_rate_limit: 120message_rate_limit: 200
Large deployments (500+ users):
Automated ingress:
smtpConnectionLimit: 50Increase Postfix limits:
connection_rate_limit: 180message_rate_limit: 300
Note: Rate limits should protect against abuse, not regulate legitimate traffic. Most legitimate mail servers send at well below these thresholds.
Verify TLS Configuration¶
Test HTTPS Access¶
# Should return 200 OK and valid TLS certificate
curl -I https://mail.example.com/admin
# Check certificate details
openssl s_client -connect mail.example.com:443 -servername mail.example.com < /dev/null 2>/dev/null | openssl x509 -noout -subject -dates
Test Mail Protocols¶
SMTP Submission (587):
openssl s_client -connect mail.example.com:587 -starttls smtp
# Should show TLS handshake success
IMAPS (993):
openssl s_client -connect mail.example.com:993
# Should show TLS handshake and IMAP greeting
SMTPS (465):
openssl s_client -connect mail.example.com:465
# Should show TLS handshake and SMTP greeting
Configure Email Client¶
Use these settings in your email client (Thunderbird, Outlook, etc.):
Incoming Mail (IMAP):
Server:
mail.example.comPort:
993Security:
SSL/TLSAuthentication:
Normal password
Outgoing Mail (SMTP):
Server:
mail.example.comPort:
587(or465)Security:
STARTTLS(587) orSSL/TLS(465)Authentication:
Normal password
Troubleshooting¶
Certificate not provisioned¶
Symptom: Certificate stuck in “Issuing” state or shows errors.
# Check certificate status
kubectl describe certificate -n mailu mailu-tls
# Check cert-manager logs
kubectl logs -n cert-manager deploy/cert-manager
# Common causes:
# - DNS not pointing to cluster
# - Firewall blocking port 80 (Let's Encrypt HTTP-01 challenge)
# - Rate limit exceeded (Let's Encrypt has rate limits)
Solution: Verify DNS and ensure port 80 is accessible from internet.
Mail client cannot connect¶
Symptom: Email client shows connection timeout or certificate errors.
# Test connectivity from outside cluster
telnet mail.example.com 587
telnet mail.example.com 993
# Check IngressRoute status
kubectl get ingressroutetcp -n mailu
# Check Traefik logs
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik
Common causes:
Traefik entry points not configured for mail ports
Firewall blocking mail ports
Service type not LoadBalancer or NodePort
TLS handshake failures¶
Symptom: openssl s_client fails or shows certificate errors.
Solution: Verify certificate secret exists and contains valid data:
# Check secret
kubectl get secret -n mailu mailu-tls
# View certificate details
kubectl get secret -n mailu mailu-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text
Webmail not accessible over HTTPS¶
Symptom: HTTPS redirects failing or webmail shows “not found”.
Solution: Check Ingress and service:
# Verify Ingress exists
kubectl get ingress -n mailu
# Check IngressRoute if using manual setup
kubectl get ingressroute -n mailu
# Test internal service
kubectl port-forward -n mailu svc/mailu-front 8080:80
curl http://localhost:8080/admin
Alternative: NodePort for Testing¶
For testing without Traefik, use NodePort service type (not recommended for production):
# Expose Front service as NodePort (after deploying with cdk8s-mailu)
kubectl patch svc -n mailu mailu-front -p '{"spec":{"type":"NodePort"}}'
# Get assigned node ports
kubectl get svc -n mailu mailu-front
# Access via http://<node-ip>:<node-port>
Note: This exposes Mailu without TLS encryption. Only use for testing.
See Also¶
Dovecot Submission Service - Understanding webmail email sending
Architecture - Component relationships
Manage Secrets - Creating TLS certificate secrets manually
Configuration Reference - Complete ingress configuration options
Traefik Documentation - IngressRoute configuration
cert-manager Documentation - Certificate provisioning