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:

  1. PROXY Protocol - Preserves real client IP addresses through Traefik

  2. Relay Restrictions - Blocks unauthorized relay attempts

  3. Rate Limiting - Prevents connection flooding

  4. ConfigMap Override - Applies PROXY protocol to Postfix master.cf

Prerequisites

Before enabling port 25:

  1. ✅ Mailu deployed and running

  2. ✅ ArgoCD managing the deployment

  3. ✅ Traefik ingress controller configured

  4. ✅ 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 IngressRouteTCP

  • smtpConnectionLimit: 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]
  1. External server connects to mail.kup6s.com:25 (Traefik LoadBalancer)

  2. Traefik wraps connection with PROXY protocol v2 header containing real client IP

  3. Postfix reads PROXY header via smtpd_upstream_proxy_protocol=haproxy

  4. Relay restrictions check real IP against mynetworks (127.0.0.1/32, 10.42.0.0/16)

  5. 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: true in config.yaml

  • Rebuild manifests: npm run build

  • Commit 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:

  1. PROXY protocol not working (Postfix sees Traefik pod IP)

  2. ConfigMap not mounted

  3. 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.