Document stable routing solution for Stalwart Mail
Add comprehensive analysis of current k3s ServiceLB issues and long-term routing solutions to prevent CNI-HOSTPORT orphaned rules. Recommended approach: Migrate to Hetzner Cloud Load Balancer (DEV-357) - Eliminates CNI-HOSTPORT complexity - True external load balancing - ~€8.91/month cost Alternative: Traefik TCP IngressRoute (interim solution) Includes recovery procedures for orphaned iptables rules. Related: DEV-359, DEV-357 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
a551ee5235
commit
38639f369e
1 changed files with 150 additions and 0 deletions
150
apps/stalwart/STABLE-ROUTING-SOLUTION.md
Normal file
150
apps/stalwart/STABLE-ROUTING-SOLUTION.md
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
# Stalwart Mail - Stable Routing Solution
|
||||||
|
|
||||||
|
**Created**: 2026-07-22
|
||||||
|
**Issue**: DEV-359
|
||||||
|
**Author**: CTO Agent
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
k3s ServiceLB (svclb) with hostPort bindings can leave orphaned CNI-HOSTPORT iptables rules when pods restart, causing intermittent mail service outages.
|
||||||
|
|
||||||
|
### Root Cause
|
||||||
|
|
||||||
|
1. **Architecture**: k3s ServiceLB uses DaemonSet pods with hostPort to expose LoadBalancer services
|
||||||
|
2. **CNI Plugin**: hostPort creates CNI-HOSTPORT iptables DNAT rules
|
||||||
|
3. **Failure Mode**: When svclb pods restart, the CNI plugin may fail to clean up old DNAT rules
|
||||||
|
4. **Impact**: External traffic gets DNATed to old (non-existent) pod IPs, causing connection timeouts
|
||||||
|
|
||||||
|
### Historical Issues
|
||||||
|
|
||||||
|
- **2026-07-20**: Stalwart pod restart left orphaned rules pointing to old svclb pod IPs
|
||||||
|
- **2026-07-22**: Orphaned rules caused complete IMAP/SMTP outage
|
||||||
|
- **Recovery**: Required manual deletion of orphaned rules + svclb pod recreation
|
||||||
|
|
||||||
|
## Recommended Long-Term Solutions
|
||||||
|
|
||||||
|
### Option 1: Migrate to Hetzner Cloud Load Balancer (RECOMMENDED)
|
||||||
|
|
||||||
|
**Architecture**:
|
||||||
|
```
|
||||||
|
External Traffic → Hetzner LB11 (public IP)
|
||||||
|
↓
|
||||||
|
k3s worker nodes (private IPs via TargetGroup)
|
||||||
|
↓
|
||||||
|
Stalwart LoadBalancer service (standard k8s)
|
||||||
|
↓
|
||||||
|
Stalwart pod
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- True external load balancing with HA
|
||||||
|
- No CNI-HOSTPORT complexity
|
||||||
|
- Automatic health checks
|
||||||
|
- SSL termination option
|
||||||
|
- Multi-node distribution
|
||||||
|
- No manual iptables management
|
||||||
|
|
||||||
|
**Cost**: ~€8.91/month (LB11 - 5 services)
|
||||||
|
|
||||||
|
**Implementation**: See `/hetzner-lb-migration-plan.md` in workspace root
|
||||||
|
|
||||||
|
**Status**: Plan approved, awaiting implementation (tracked in DEV-357)
|
||||||
|
|
||||||
|
### Option 2: Traefik TCP IngressRoute (ALTERNATIVE)
|
||||||
|
|
||||||
|
**Architecture**:
|
||||||
|
```
|
||||||
|
External Traffic → Traefik (ports 25, 587, 993)
|
||||||
|
↓ (TCP routing via IngressRouteTCP)
|
||||||
|
Stalwart ClusterIP service
|
||||||
|
↓
|
||||||
|
Stalwart pod
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- No additional infrastructure
|
||||||
|
- Traefik already deployed
|
||||||
|
- Unified ingress for HTTP + TCP
|
||||||
|
- No CNI-HOSTPORT dependencies
|
||||||
|
|
||||||
|
**Drawbacks**:
|
||||||
|
- Requires Traefik configuration changes
|
||||||
|
- Must expose additional ports on Traefik service
|
||||||
|
- Single point of failure (unless Traefik is HA)
|
||||||
|
|
||||||
|
**Implementation Steps**:
|
||||||
|
1. Add SMTP/IMAP entryPoints to Traefik deployment args
|
||||||
|
2. Add ports 25, 587, 993 to Traefik LoadBalancer service
|
||||||
|
3. Create IngressRouteTCP resources (already exist in cluster)
|
||||||
|
4. Change Stalwart services from LoadBalancer to ClusterIP
|
||||||
|
|
||||||
|
**Status**: Not implemented, IngressRouteTCP resources exist but Traefik entryPoints missing
|
||||||
|
|
||||||
|
### Option 3: MetalLB (NOT RECOMMENDED)
|
||||||
|
|
||||||
|
Installing MetalLB to provide real LoadBalancer IPs.
|
||||||
|
|
||||||
|
**Drawbacks**:
|
||||||
|
- Additional complexity
|
||||||
|
- Potential conflicts with k3s ServiceLB
|
||||||
|
- Still uses node IPs (no true external LB)
|
||||||
|
- Requires IP pool management
|
||||||
|
|
||||||
|
**Status**: Rejected - Hetzner LB or Traefik are better options
|
||||||
|
|
||||||
|
## Current State (2026-07-22)
|
||||||
|
|
||||||
|
**Active Solution**: k3s ServiceLB with hostPort bindings
|
||||||
|
|
||||||
|
**Configuration**:
|
||||||
|
- Service type: LoadBalancer with `externalTrafficPolicy: Local`
|
||||||
|
- svclb DaemonSet pods on all nodes
|
||||||
|
- CNI-HOSTPORT iptables rules for port forwarding
|
||||||
|
- Exposed ports: SMTP (25, 587), IMAPS (993)
|
||||||
|
- Removed insecure ports: SMTPS (465), IMAP (143)
|
||||||
|
|
||||||
|
**Known Issues**:
|
||||||
|
- CNI-HOSTPORT rules can become orphaned on pod restart
|
||||||
|
- Requires manual intervention to clean up orphaned rules
|
||||||
|
- Not truly multi-node load balanced
|
||||||
|
|
||||||
|
**Recovery Procedure** (if orphaned rules occur):
|
||||||
|
```bash
|
||||||
|
# 1. Delete all svclb pods to trigger CNI cleanup
|
||||||
|
kubectl delete pods -n kube-system -l 'svccontroller.k3s.cattle.io/svcname=stalwart-smtp'
|
||||||
|
kubectl delete pods -n kube-system -l 'svccontroller.k3s.cattle.io/svcname=stalwart-imap'
|
||||||
|
|
||||||
|
# 2. Wait for pods to recreate (CNI will create new hostPort rules)
|
||||||
|
kubectl wait --for=condition=ready pods -n kube-system -l 'svccontroller.k3s.cattle.io/svcname=stalwart-smtp' --timeout=60s
|
||||||
|
|
||||||
|
# 3. Verify CNI-HOSTPORT rules on each node
|
||||||
|
for node in k3s-cp-1 k3s-worker-1 k3s-worker-2 k3s-worker-3; do
|
||||||
|
ssh root@$node "iptables-legacy -t nat -L CNI-HOSTPORT-DNAT -n"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 4. Test external connectivity
|
||||||
|
telnet mail.basicstack.de 587
|
||||||
|
openssl s_client -connect mail.basicstack.de:993
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
**Implement Option 1 (Hetzner Load Balancer)** for the following reasons:
|
||||||
|
|
||||||
|
1. **Stability**: Eliminates CNI-HOSTPORT complexity entirely
|
||||||
|
2. **Scalability**: True external load balancing across multiple nodes
|
||||||
|
3. **Reliability**: Hetzner-managed infrastructure with automatic health checks
|
||||||
|
4. **Cost-effective**: €8.91/month is acceptable for a production mail service
|
||||||
|
5. **Future-proof**: Enables future HA Stalwart deployments
|
||||||
|
|
||||||
|
**Timeline**: Can be implemented in ~1 day (see DEV-357 migration plan)
|
||||||
|
|
||||||
|
**Fallback**: If budget/approval is blocked, implement Option 2 (Traefik TCP) as interim solution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- Migration Plan: `/hetzner-lb-migration-plan.md`
|
||||||
|
- Implementation Task: DEV-357
|
||||||
|
- Historical Investigation: `apps/stalwart/ISSUE-2026-07-13-smtp-imap-external-access.md`
|
||||||
Loading…
Add table
Reference in a new issue