Nginx Configuration Patches

How cdk8s-mailu patches nginx configuration for Traefik TLS termination.

Introduction

When using TLS_FLAVOR=notls with Traefik TLS termination, the Front nginx container requires configuration patches to:

  1. Add mail protocol listeners for ports 587, 465, 993, 995

  2. Configure auth_http to use Admin service instead of localhost

This document explains the patching mechanism and what changes are applied.

Note: In cdk8s-mailu, Traefik routes HTTP traffic directly to Admin and Webmail services, bypassing Front nginx entirely. Therefore, HTTP location blocks and redirects are not needed and not applied.

Patch Architecture

Why Patching is Needed

Mailu’s Default Behavior:

  • TLS_FLAVOR=notls only configures HTTP on port 80

  • Mail protocol ports (587, 465, 993, 995) are NOT created

  • Assumes TLS handled internally or not needed

  • auth_http points to localhost:8000 (doesn’t work in Kubernetes multi-pod architecture)

cdk8s-mailu Requirements:

  • Traefik terminates TLS and forwards plaintext to nginx (for mail protocols only)

  • nginx must listen on mail protocol ports to receive from Traefik

  • nginx must authenticate via Admin service (separate pod)

  • nginx must proxy to backend services after authentication (Postfix, Dovecot)

Solution: Wrapper script patches generated nginx.conf before nginx starts.

HTTP Traffic: Traefik routes HTTP directly to Admin and Webmail services, bypassing nginx completely.

Patch Delivery Mechanism

ConfigMap → Init Container → Shared Volume:

  1. NginxPatchConfigMap construct creates ConfigMap with wrapper script

  2. Front Deployment mounts ConfigMap at /patch/entrypoint-wrapper.sh

  3. Init Container copies script to shared volume and makes executable:

    initContainers:
    - name: copy-entrypoint
      image: busybox
      command: ['sh', '-c', 'cp /patch/entrypoint-wrapper.sh /entrypoint/entrypoint-wrapper.sh && chmod +x /entrypoint/entrypoint-wrapper.sh']
      volumeMounts:
      - name: entrypoint-volume
        mountPath: /entrypoint
      - name: nginx-patch
        mountPath: /patch
    
  4. Main Container runs wrapper script instead of original entrypoint:

    command: ['/entrypoint/entrypoint-wrapper.sh']
    

Why Init Container?

  • ConfigMap files are read-only (cannot chmod +x directly)

  • Shared emptyDir volume allows executable script

  • Clean separation: init copies once, main container runs

Patch Script Workflow

The wrapper script executes in three phases:

Phase 1: Generate Base Configuration

# Run Mailu's config.py to generate nginx.conf from templates
python3 /config.py

What config.py does:

  • Reads environment variables (TLS_FLAVOR, HOSTNAMES, etc.)

  • Generates /etc/nginx/nginx.conf from Jinja2 templates

  • Creates base mail protocol listeners (port 25 only with TLS_FLAVOR=notls)

  • Creates base HTTP server block for port 80

Output: /etc/nginx/nginx.conf with basic configuration

Phase 2: Apply Patches

Two patches are applied to the generated nginx.conf:

Patch 1: Fix auth_http Endpoint

Problem: Default config points to http://127.0.0.1:8000/auth/email

  • Doesn’t work in Kubernetes (Admin is separate pod)

  • Wrong endpoint path

Patch:

sed -i "s|auth_http http://127.0.0.1:8000/auth/email;|auth_http http://\${ADMIN_ADDRESS}:8080/internal/auth/email;|g" "$NGINX_CONF"

Changes:

  • 127.0.0.1:8000${ADMIN_ADDRESS}:8080 (Kubernetes service DNS)

  • /auth/email/internal/auth/email (correct Mailu endpoint)

Result:

auth_http http://admin-service.mailu.svc.cluster.local:8080/internal/auth/email;

Patch 2: Inject Mail Protocol Listeners

Problem: TLS_FLAVOR=notls doesn’t create listeners for 587, 465, 993, 995

Patch: Inserts four server blocks into mail{} section after port 25 block:

# Submission (port 587) for Traefik TLS termination
server {
  listen 587;
  protocol smtp;
  smtp_auth plain;
  auth_http_header Auth-Port 587;
  auth_http_header Client-Port $remote_port;
}

# SMTPS (port 465) for Traefik TLS termination
server {
  listen 465;
  protocol smtp;
  smtp_auth plain;
  auth_http_header Auth-Port 465;
  auth_http_header Client-Port $remote_port;
}

# IMAPS (port 993) for Traefik TLS termination
server {
  listen 993;
  protocol imap;
  imap_auth plain;
  auth_http_header Auth-Port 993;
  auth_http_header Client-Port $remote_port;
}

# POP3S (port 995) for Traefik TLS termination
server {
  listen 995;
  protocol pop3;
  pop3_auth plain;
  auth_http_header Auth-Port 995;
  auth_http_header Client-Port $remote_port;
}

Key Configuration:

  • protocol smtp/imap/pop3: nginx mail module protocol handlers

  • smtp_auth plain / imap_auth plain / pop3_auth plain: Enable PLAIN authentication

  • auth_http_header Auth-Port: Tells Admin which port client connected to

  • auth_http_header Client-Port: Passes client’s source port for logging

Authentication Flow:

  1. Client sends credentials (username/password)

  2. nginx extracts credentials and sends to auth_http endpoint

  3. Admin validates and returns backend address

  4. nginx proxies connection to backend (Postfix or Dovecot)

Phase 3: Verify and Start

# Verify patches were applied
if ! grep -q "# Submission (port 587) for Traefik TLS termination" "$NGINX_CONF"; then
  echo "ERROR: Mail protocol patches not found in $NGINX_CONF"
  exit 1
fi

echo "Patch verification: OK - All patches applied successfully"

# Start nginx in foreground
exec /usr/sbin/nginx -g "daemon off;"

Verification checks:

  • Mail protocol listeners added (ports 587, 465, 993, 995)

  • Fails with error if patches not found

Failure handling: Exit immediately if patches not applied (fail-fast approach)

Configuration File Structure

Before Patching (config.py output)

# nginx.conf generated by Mailu config.py with TLS_FLAVOR=notls

mail {
    server {
        listen 25;
        protocol smtp;
        auth_http http://127.0.0.1:8000/auth/email;  # Wrong: localhost
    }
    # No 587, 465, 993, 995 listeners
}

http {
    server {
        listen 80;
        # HTTP server (not used - Traefik routes directly to services)
    }
}

After Patching (wrapper script output)

# nginx.conf after cdk8s-mailu patches applied

mail {
    server {
        listen 25;
        protocol smtp;
        auth_http http://admin-service.mailu.svc.cluster.local:8080/internal/auth/email;  # Fixed
    }

    # NEW: Submission listener
    server {
        listen 587;
        protocol smtp;
        smtp_auth plain;
        auth_http_header Auth-Port 587;
    }

    # NEW: SMTPS listener
    server {
        listen 465;
        protocol smtp;
        smtp_auth plain;
        auth_http_header Auth-Port 465;
    }

    # NEW: IMAPS listener
    server {
        listen 993;
        protocol imap;
        imap_auth plain;
        auth_http_header Auth-Port 993;
    }

    # NEW: POP3S listener
    server {
        listen 995;
        protocol pop3;
        pop3_auth plain;
        auth_http_header Auth-Port 995;
    }
}

http {
    server {
        listen 80;
        # HTTP server (not used - Traefik routes directly to services)
    }
}

Integration with Kubernetes

Service Port Exposure

Front Service exposes only TLS-terminated mail protocol ports (no HTTP or plaintext mail ports):

apiVersion: v1
kind: Service
metadata:
  name: front-service
spec:
  ports:
  # TLS-terminated SMTP ports (Traefik terminates TLS, forwards plaintext to Front)
  - name: smtps
    port: 465
    targetPort: 465
  - name: submission
    port: 587
    targetPort: 587
  # TLS-terminated IMAP port
  - name: imaps
    port: 993
    targetPort: 993
  # TLS-terminated POP3 port
  - name: pop3s
    port: 995
    targetPort: 995

Not exposed (routed differently by Traefik):

  • Port 25 (SMTP): Traefik routes directly to Postfix:25

  • Port 80/443 (HTTP/HTTPS): Traefik routes directly to Admin:8080 and Webmail:80

  • Port 143/110 (IMAP/POP3): Plaintext protocols disabled (use IMAPS/POP3S instead)

Traefik Configuration

Required Traefik EntryPoints (configured in cluster infrastructure):

  • smtps (465) - SMTPS with TLS termination

  • smtp-submission (587) - Submission with TLS termination (or passthrough)

  • imaps (993) - IMAPS with TLS termination

  • pop3s (995) - POP3S with TLS termination

Example IngressRouteTCP (created by TraefikIngressConstruct):

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
  name: mailu-submission
spec:
  entryPoints:
    - smtp-submission  # 587
  routes:
  - match: HostSNI(`*`)
    services:
    - name: front-service
      port: 587
  # Note: Submission (587) typically uses STARTTLS, not TLS wrapping
  # So TLS section may be omitted (passthrough)

Example with TLS termination (SMTPS/IMAPS/POP3S):

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
  name: mailu-smtps
spec:
  entryPoints:
    - smtps  # 465
  routes:
  - match: HostSNI(`*`)
    services:
    - name: front-service
      port: 465
  tls:
    secretName: mailu-tls
    options:
      name: mailu-mail-tls

Traffic Flow (TLS-terminated mail protocols):

  1. Mail client connects to Traefik:465/587/993/995 with TLS

  2. Traefik terminates TLS (for 465/993/995; passthrough for 587)

  3. Traefik forwards plaintext to Front:465/587/993/995

  4. Front nginx receives on patched listener

  5. Front nginx authenticates via Admin service

  6. Front nginx proxies to backend (Postfix or Dovecot)

Environment Variables Used

Wrapper script relies on these environment variables:

  • ADMIN_ADDRESS: Admin service DNS name (set by MailuChart)

  • TLS_FLAVOR: Must be “notls” for patches to make sense

  • HOSTNAMES: Comma-separated list of domains (used by config.py)

Troubleshooting

Patch Not Applied

Symptoms: nginx fails to start, or mail clients get “connection refused”

Check:

  1. Verify ConfigMap exists:

    kubectl get configmap -n mailu mailu-nginx-patch-configmap
    
  2. Check wrapper script is executable:

    kubectl exec -n mailu deployment/front-deployment -- ls -l /entrypoint/entrypoint-wrapper.sh
    
  3. Check init container logs:

    kubectl logs -n mailu deployment/front-deployment -c copy-entrypoint
    
  4. Check main container logs for patch verification:

    kubectl logs -n mailu deployment/front-deployment | grep "Patch verification"
    

Authentication Fails on Mail Protocols

Symptoms: Mail clients get “Authentication failed” on ports 587/993/995

Check:

  1. Verify auth_http endpoint is correct:

    kubectl exec -n mailu deployment/front-deployment -- cat /etc/nginx/nginx.conf | grep auth_http
    

    Should show: auth_http http://admin-service....:8080/internal/auth/email;

  2. Test Admin endpoint from Front pod:

    kubectl exec -n mailu deployment/front-deployment -- wget -O- http://admin-service:8080/internal/auth/email
    

nginx Configuration Syntax Error

Symptoms: nginx fails to start with “configuration file test failed”

Check:

  1. View full nginx configuration:

    kubectl exec -n mailu deployment/front-deployment -- cat /etc/nginx/nginx.conf
    
  2. Test nginx configuration manually:

    kubectl exec -n mailu deployment/front-deployment -- nginx -t
    

Common issues:

  • sed pattern didn’t match (Mailu version changed template structure)

  • Escaping issues in patch script

  • Duplicate server blocks

HTTP Traffic Not Going Through Front

This is expected behavior in cdk8s-mailu:

  • Traefik routes HTTP directly to Admin:8080 and Webmail:80

  • Front nginx only handles mail protocols (SMTP, IMAP, POP3)

  • This is by design for simplified architecture

  • No HTTP location blocks or redirects are configured in Front nginx

Code Reference

Source: src/constructs/nginx-patch-configmap.ts

Key Components:

  • NginxPatchConfigMap class: Creates ConfigMap with wrapper script

  • wrapperScript variable: Complete bash script with all patches

  • sed patterns: Text transformations applied to nginx.conf

Usage in MailuChart:

// Create nginx patch ConfigMap
this.nginxPatchConfigMap = new NginxPatchConfigMap(this, 'nginx-patch', {
  namespace: this.namespace,
});

// Mount in Front deployment
this.frontConstruct = new FrontConstruct(this, 'front', {
  config: this.config,
  namespace: this.namespace,
  sharedConfigMap: this.sharedConfigMap,
  nginxPatchConfigMap: this.nginxPatchConfigMap.configMap,
});

See Also