Enable SMTP Port 25 Securely¶
This guide explains how to safely enable SMTP port 25 for receiving email from external mail servers while preventing unauthorized relay abuse.
Overview¶
Port 25 is required to receive email from external mail servers (MX record delivery). However, exposing port 25 without proper security creates an open relay vulnerability, allowing spammers to send unlimited emails through your server.
Security Requirements:
PROXY Protocol - Preserves real client IP addresses through Traefik
Relay Restrictions - Blocks unauthorized relay attempts
Rate Limiting - Prevents connection flooding
ConfigMap Override - Applies PROXY protocol to Postfix master.cf
Prerequisites¶
Before enabling port 25:
✅ Mailu deployed and running
✅ ArgoCD managing the deployment
✅ Traefik ingress controller configured
✅ Understanding of mail relay security
Step 1: Create Postfix Override ConfigMap¶
The PROXY protocol configuration requires a ConfigMap that modifies Postfix’s master.cf file.
Create the ConfigMap:
# Create postfix.master override file
cat > /tmp/postfix.master <<'EOF'
smtp/inet=smtp inet n - n - - smtpd -o smtpd_upstream_proxy_protocol=haproxy
EOF
# Create ConfigMap in mailu namespace
kubectl create configmap postfix-master-override \
-n mailu \
--from-file=postfix.master=/tmp/postfix.master
What this does:
Configures Postfix to accept PROXY protocol v2 headers from Traefik
Extracts real client IP from PROXY headers instead of seeing Traefik pod IP
Enables relay restrictions to work correctly based on real client IP
Verification:
# Verify ConfigMap exists
kubectl get configmap postfix-master-override -n mailu
# Check content
kubectl get configmap postfix-master-override -n mailu -o yaml
Step 2: Enable SMTP in Configuration¶
Edit dp-infra/mailu/config.yaml:
ingress:
enabled: true
type: "traefik"
traefik:
hostname: "mail.kup6s.com"
certIssuer: "letsencrypt-cluster-issuer"
enableTcp: true
smtpConnectionLimit: 15
enableSmtp: true # ← Enable port 25 (default: false)
Configuration Options:
enableSmtp: true- Enables SMTP port 25 IngressRouteTCPsmtpConnectionLimit: 15- Maximum concurrent connections per IP (default: 15)
Security Note: enableSmtp defaults to false for security. Only enable if you need to receive email from external servers.
Step 3: Rebuild and Deploy¶
cd dp-infra/mailu
# Rebuild manifests with new configuration
npm run build
# Review changes
git diff manifests/mailu.k8s.yaml
# Commit and push
git add config.yaml manifests/
git commit -m "Enable SMTP port 25 with PROXY protocol security"
git push
Step 4: Sync with ArgoCD¶
# Trigger manual sync
kubectl annotate application mailu-app-c831ae01 \
-n argocd \
argocd.argoproj.io/refresh=normal \
--overwrite
# Wait for sync to complete (check ArgoCD UI)
# Or manually sync via ArgoCD CLI:
# argocd app sync mailu-app
Step 5: Verify Deployment¶
Check Postfix Pod:
# Wait for new pod to start
kubectl get pods -n mailu | grep postfix
# Should show: 1/1 Running
Verify ConfigMap Mount:
POD=$(kubectl get pod -n mailu -l app.kubernetes.io/component=postfix -o jsonpath='{.items[0].metadata.name}')
# Check ConfigMap is mounted
kubectl exec -n mailu $POD -- ls -la /overrides/
# Should show: postfix.master
Verify Postfix Configuration:
# Check master.cf has PROXY protocol enabled
kubectl exec -n mailu $POD -- cat /etc/postfix/master.cf | grep -A 2 "^smtp.*inet"
# Expected output:
# smtp inet n - n - - smtpd
# -o smtpd_upstream_proxy_protocol=haproxy
Step 6: Test Security¶
Run the security test to verify relay restrictions work:
# Test 1: Try unauthorized relay (should be REJECTED)
(
sleep 1; echo "EHLO test.example.com"
sleep 1; echo "MAIL FROM:<test@gmail.com>"
sleep 1; echo "RCPT TO:<test@yahoo.com>"
sleep 1; echo "QUIT"
) | timeout 10 nc mail.kup6s.com 25
# Expected: "554 5.7.1 <test@yahoo.com>: Relay access denied" ✅
# Test 2: Try delivery to local domain (should be ACCEPTED)
(
sleep 1; echo "EHLO test.example.com"
sleep 1; echo "MAIL FROM:<sender@example.com>"
sleep 1; echo "RCPT TO:<postmaster@kup6s.com>"
sleep 1; echo "QUIT"
) | timeout 10 nc mail.kup6s.com 25
# Expected: "250 2.1.5 Ok" ✅
# (May reject sender domain if invalid, but won't reject as relay)
✅ Security Verified: If test 1 shows “Relay access denied”, the server is secure!
Step 7: Monitor for Abuse¶
After enabling port 25, monitor for spam activity:
# Check mail queue size
kubectl exec -n mailu $POD -- mailq | tail -1
# Should show: "Mail queue is empty" or small number
# Check Postfix logs for relay attempts
kubectl logs -n mailu $POD --tail=100 | grep "Relay access denied"
# Set up alerts (optional)
# See: Set up monitoring alerts for mail queue size
How It Works¶
PROXY Protocol Flow¶
Internet → Traefik (port 25) → Postfix (pod)
[sends PROXY v2] [reads real IP]
External server connects to
mail.kup6s.com:25(Traefik LoadBalancer)Traefik wraps connection with PROXY protocol v2 header containing real client IP
Postfix reads PROXY header via
smtpd_upstream_proxy_protocol=haproxyRelay restrictions check real IP against
mynetworks(127.0.0.1/32, 10.42.0.0/16)External IPs not in mynetworks → relay DENIED ✅
Without PROXY Protocol (Vulnerable!)¶
Internet → Traefik (pod IP: 10.42.x.x) → Postfix
[no PROXY header] [sees 10.42.x.x]
[10.42.x.x in mynetworks!]
[relay ALLOWED ❌]
All connections appear to come from Traefik pod IP (10.42.x.x), which is in mynetworks, so relay restrictions are bypassed!
Security Configuration Explained¶
Relay Restrictions¶
Configured in mailu-chart.ts:
// RELAYNETS: Networks allowed to relay without authentication
// Empty = no external relaying allowed
envVars.RELAYNETS = '';
This sets Postfix’s mynetworks to:
mynetworks = 127.0.0.1/32 10.42.0.0/16
Only localhost and Kubernetes pod network can relay (internal authenticated traffic via ports 465/587).
Rate Limiting¶
Configured at two levels:
1. Traefik Connection Limit (per IP):
smtpConnectionLimit: 15 # Max concurrent connections
2. Postfix Rate Limits (via environment variables):
// Connection rate limiting (Postfix anvil)
POSTFIX_smtpd_client_connection_rate_limit: "60" // 60 connections/min per IP
POSTFIX_smtpd_client_connection_count_limit: "10" // 10 simultaneous connections per IP
POSTFIX_smtpd_client_message_rate_limit: "100" // 100 messages/min per IP
POSTFIX_smtpd_client_recipient_rate_limit: "300" // 300 recipients/min per IP
Note: These environment variables are for documentation only. Postfix doesn’t apply them automatically. Actual rate limiting is enforced by Traefik connection limits and Mailu’s built-in controls.
Authentication Requirements¶
Port 25 (SMTP): NO authentication required (standard for MX delivery)
Port 465 (SMTPS): TLS + SASL authentication required
Port 587 (Submission): STARTTLS + SASL authentication required
Users send mail via authenticated ports (465/587), never port 25!
Troubleshooting¶
SMTP Route Not Created¶
Symptom: Cannot connect to port 25, nc mail.kup6s.com 25 hangs
Check:
kubectl get ingressroutetcp -n mailu | grep smtp
Should show: mailu-smtp
If missing:
Verify
enableSmtp: truein config.yamlRebuild manifests:
npm run buildCommit and push
Trigger ArgoCD sync
“Bad Syntax” Errors¶
Symptom: Postfix responds with “500 5.5.2 Error: bad syntax”
Cause: PROXY protocol not configured on Postfix side
Fix:
# Verify ConfigMap exists
kubectl get configmap postfix-master-override -n mailu
# Check ConfigMap is mounted
kubectl exec -n mailu $POD -- cat /overrides/postfix.master
# Should show: smtp/inet=smtp inet n - n - - smtpd -o smtpd_upstream_proxy_protocol=haproxy
If missing: Recreate ConfigMap and restart Postfix pod
Relay Still Allowed¶
Symptom: Security test shows relay accepted (250 OK instead of 554 denied)
Causes:
PROXY protocol not working (Postfix sees Traefik pod IP)
ConfigMap not mounted
Postfix configuration not applied
Debug:
# Check actual mynetworks
kubectl exec -n mailu $POD -- postconf mynetworks
# Should show: 127.0.0.1/32 10.42.0.0/16
# Check PROXY protocol config
kubectl exec -n mailu $POD -- postconf -Mf smtp/inet | grep upstream
# Should show: -o smtpd_upstream_proxy_protocol=haproxy
# Check Postfix is using real IP (check logs during test)
kubectl logs -n mailu $POD --tail=20 | grep "client="
# Should show real external IP, NOT 10.42.x.x
Rollback¶
If you need to disable port 25:
cd dp-infra/mailu
# Edit config.yaml
# Set: enableSmtp: false
# Rebuild
npm run build
# Commit and push
git add config.yaml manifests/
git commit -m "Disable SMTP port 25"
git push
# Sync with ArgoCD
kubectl annotate application mailu-app-c831ae01 -n argocd \
argocd.argoproj.io/refresh=normal --overwrite
This removes the SMTP IngressRouteTCP, blocking external access to port 25.