Dovecot Submission Service for Webmail¶
This document explains the dedicated dovecot submission service that enables email sending from Roundcube webmail.
The Problem: Webmail SMTP Authentication¶
When a user composes an email in webmail (Roundcube) and clicks “Send”, the webmail backend needs to submit that email to the SMTP server (postfix). This requires proper authentication to prevent unauthorized email sending.
Mailu’s standard architecture uses the bundled dovecot service in the front container for this purpose, but with TLS_FLAVOR=notls (required to avoid HTTP redirect loops), configuring this bundled dovecot becomes extremely difficult because:
The front container’s dovecot configuration is deeply embedded in Mailu’s startup scripts
Environment variable substitution doesn’t work properly with dovecot syntax
The configuration files are generated at runtime in read-only locations
Modifying the bundled dovecot requires extensive wrapper script modifications
The Solution: Dedicated Dovecot Submission Service¶
Instead of trying to configure the bundled dovecot in the front container, we deploy a separate dovecot submission service using the official dovecot/dovecot:2.3-latest image with custom configuration.
Architecture¶
Webmail (Roundcube)
↓ (PLAIN auth with token, port 10025)
Dovecot Submission Service
- Accepts: nopassword=y (static passdb)
- User: uid=mail (8), gid=mail
- Mail location: maildir:/tmp/mail
↓ (submission_relay_host, no auth, port 25)
Postfix
- Trusts: mynetworks (10.42.0.0/16 pod network)
- Accepts: plaintext from pod network
↓
Email Delivery ✅
Key Components¶
1. Dovecot Submission Service (dovecot-submission-construct.ts)
Image:
dovecot/dovecot:2.3-latest(official upstream image)Port: 10025 (internal submission with token authentication)
Architecture: AMD64 only (official image doesn’t support ARM64)
Node Placement: Scheduled to AMD64 node with appropriate nodeSelector and toleration
Configuration highlights:
# Protocols - only submission
protocols = submission
# Allow low UIDs (mail user is UID 8)
first_valid_uid = 8
last_valid_uid = 0
# Mail location (relay-only, no actual storage needed)
mail_location = maildir:/tmp/mail
# Submission relay configuration
submission_relay_host = postfix.mailu.svc.cluster.local
submission_relay_port = 25
submission_relay_trusted = yes
submission_relay_ssl = no
# Authentication via static passdb (token auth handled by webmail, accept all)
passdb {
driver = static
args = nopassword=y
}
# User database (static, minimal config for relay)
userdb {
driver = static
args = uid=mail gid=mail home=/tmp
}
2. Environment Variable Substitution
Dovecot doesn’t support shell-style ${VAR} syntax natively. To work around this, we use an entrypoint wrapper script that:
Uses
sedto substitute placeholders (DOMAIN_PLACEHOLDER,SMTP_ADDRESS_PLACEHOLDER)Validates the generated configuration with
doveconf -cStarts dovecot with the generated config
3. Service Discovery
The dovecot submission service is registered in the shared ConfigMap as SUBMISSION_ADDRESS:
if (this.dovecotSubmissionConstruct?.service) {
this.sharedConfigMap.addData(
'SUBMISSION_ADDRESS',
`${this.dovecotSubmissionConstruct.service.name}.${namespace}.svc.cluster.local`
);
}
Webmail uses this environment variable to connect to the correct service (handling CDK8S’s hash-based service names).
Authentication Flow¶
User sends email from webmail: Roundcube submits to
${SUBMISSION_HOST}:10025(resolved fromSUBMISSION_ADDRESSenv var)Dovecot authenticates: Static passdb with
nopassword=yaccepts the connection (token validation happens at webmail level, not dovecot)Dovecot relays to postfix: Using
submission_relay_host, dovecot forwards topostfix:25without authentication (trusted network)Postfix accepts and delivers: Postfix trusts connections from the pod network (
10.42.0.0/16) and delivers the email
Why This Approach Works¶
No Postfix Authentication Required: Postfix doesn’t need to authenticate dovecot because:
Connections come from trusted pod network (
mynetworks = 10.42.0.0/16)Only the dovecot submission service can connect to postfix:25 from within the cluster
Webmail has already authenticated the user via Mailu’s SSO
Token Authentication at Webmail Level: Roundcube uses Mailu’s session tokens, so by the time a request reaches dovecot submission:
User is already authenticated
Dovecot just needs to relay (not validate credentials)
Static
nopassword=ypassdb is sufficient
Separate Service Isolation: Using a dedicated service instead of the bundled dovecot:
Simplifies configuration (clean dovecot.conf instead of patching Mailu’s templates)
Enables easy troubleshooting (dedicated pod with clear logs)
Allows independent scaling and resource management
Avoids conflicts with Mailu’s internal dovecot usage
Critical Configuration Details¶
UID Validation¶
Dovecot’s default first_valid_uid is typically 500 or 1000, but Mailu’s mail user has UID 8. Without first_valid_uid = 8, dovecot rejects logins with:
Mail access for users with UID 8 not permitted (see first_valid_uid in config file)
Mail Storage¶
Even for relay-only services, dovecot requires mail_location to be set. Without it, errors occur:
mail_location not set and autodetection failed: Mail storage autodetection failed with home=/tmp
We use mail_location = maildir:/tmp/mail as a minimal configuration (no actual mail is stored).
Filesystem Permissions¶
The container’s /etc/dovecot/ directory is read-only. To work around this:
Mount ConfigMap with templates to
/etc/dovecot/config/(read-only,defaultMode: 0o755for executable entrypoint)Generate runtime config in writable
/var/run/dovecot/runtime/directoryStart dovecot with
-c /var/run/dovecot/runtime/dovecot.conf
Important: Avoid using /var/run/dovecot/config as the directory name - dovecot has an internal “config” service that conflicts with a directory by that name, causing errors:
service(config): unlink(/run/dovecot/config) failed: Is a directory
AMD64 Architecture Requirement¶
The official dovecot/dovecot:2.3-latest image only supports AMD64 architecture. For clusters with ARM64 nodes (like kup6s), we need:
Node Selector:
nodeSelector:
kubernetes.io/arch: amd64
Toleration (for AMD64 taint):
tolerations:
- key: kubernetes.io/arch
operator: Equal
value: amd64
effect: NoSchedule
This ensures the dovecot submission pod is scheduled to the AMD64 node.
Webmail Configuration¶
The webmail patch script (webmail-patch-configmap.ts) is updated to use the dovecot submission service:
SUBMISSION_HOST="${SUBMISSION_ADDRESS:-dovecot-submission}"
# Patch SMTP host: Use SUBMISSION_ADDRESS:10025 (dedicated dovecot submission service with token auth)
sed -i "s|tls://[^:]*:10025|smtp://${SUBMISSION_HOST}:10025|g" "$RC_CONFIG"
This replaces the hardcoded FRONT_ADDRESS:10025 reference with the dynamically discovered dovecot submission service.
Troubleshooting¶
Dovecot pod stuck in CrashLoopBackOff¶
Check logs:
kubectl logs -n mailu -l app.kubernetes.io/component=dovecot-submission --tail=50
Common issues:
Invalid configuration: Dovecot config validation failed
Check for syntax errors in dovecot.conf template
Verify environment variable substitution worked correctly
Run
doveconf -c /var/run/dovecot/runtime/dovecot.confin the pod
Architecture mismatch: Pod scheduled to ARM64 node
Verify nodeSelector and toleration are applied
Check node labels:
kubectl get nodes --show-labels | grep arch
UID errors:
first_valid_uidnot set correctlyEnsure
first_valid_uid = 8is in the configuration
Mail storage errors:
mail_locationmissingEnsure
mail_location = maildir:/tmp/mailis set
Webmail can’t send email (SMTP Error)¶
Check webmail logs:
kubectl logs -n mailu -l app.kubernetes.io/component=webmail --tail=50 | grep -i smtp
Verify service discovery:
kubectl get configmap -n mailu mailu-shared-config-* -o yaml | grep SUBMISSION_ADDRESS
Should show the full service name (e.g., mailu-dovecot-submission-service-c88c85f9.mailu.svc.cluster.local).
Test connection from webmail pod:
POD=$(kubectl get pod -n mailu -l app.kubernetes.io/component=webmail -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n mailu $POD -- nc -zv mailu-dovecot-submission-service-c88c85f9 10025
Should connect successfully.
Email relay failing at postfix¶
Check postfix logs:
kubectl logs -n mailu -l app.kubernetes.io/component=postfix --tail=50
Verify postfix trusts pod network:
kubectl exec -n mailu <postfix-pod> -- postconf mynetworks
Should include 10.42.0.0/16 or the cluster’s pod CIDR.
Implementation Files¶
Construct:
generic-charts/cdk8s-mailu/src/constructs/dovecot-submission-construct.tsChart Integration:
generic-charts/cdk8s-mailu/src/mailu-chart.ts(createDovecotSubmissionComponent)Webmail Patch:
generic-charts/cdk8s-mailu/src/constructs/webmail-patch-configmap.tsPostfix Service:
generic-charts/cdk8s-mailu/src/constructs/postfix-construct.ts(added port 10025)