diff --git a/apps/stalwart/README.md b/apps/stalwart/README.md index 3df5d19..098044f 100644 --- a/apps/stalwart/README.md +++ b/apps/stalwart/README.md @@ -41,6 +41,8 @@ After deployment, log in at https://mail.basicstack.de with: ## Configuration +> **IMPORTANT:** The configuration is stored in the RocksDB and cannot be overwritten by a configuration file! + All configuration is done via the web UI or API. The bootstrap config only points to the RocksDB database location. NO config.toml files are used. Check https://stalw.art/docs/ref/ for configuration possibilities. diff --git a/apps/stalwart/stalwart-allow-cluster-ips-job.yaml b/apps/stalwart/stalwart-allow-cluster-ips-job.yaml new file mode 100644 index 0000000..be791c3 --- /dev/null +++ b/apps/stalwart/stalwart-allow-cluster-ips-job.yaml @@ -0,0 +1,123 @@ +--- +# One-time Job to configure Stalwart HTTP listener to allow internal cluster IPs +# +# Problem: Stalwart blocks the HTTP port from Traefik's pod IP (10.244.2.227), +# causing 502/503 errors when accessing mail.basicstack.de +# +# Solution: Use kubectl exec to access Stalwart's admin API via localhost (which is allowed) +# and disable IP filtering for the HTTP listener, or allow the pod network CIDR +# +# This Job must be manually triggered after Stalwart is running: +# kubectl create job --from=cronjob/stalwart-allow-cluster-ips manual-fix -n stalwart +# +# Or apply directly: +# kubectl apply -f stalwart-allow-cluster-ips-job.yaml +# kubectl wait --for=condition=complete job/stalwart-allow-cluster-ips -n stalwart --timeout=120s +# kubectl logs -n stalwart job/stalwart-allow-cluster-ips +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: stalwart-config-access + namespace: stalwart +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: stalwart-config-access + namespace: stalwart +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] +- apiGroups: [""] + resources: ["pods/exec"] + verbs: ["create"] +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get"] + resourceNames: ["stalwart-admin-credentials"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: stalwart-config-access + namespace: stalwart +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: stalwart-config-access +subjects: +- kind: ServiceAccount + name: stalwart-config-access + namespace: stalwart +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: stalwart-allow-cluster-ips + namespace: stalwart +spec: + ttlSecondsAfterFinished: 3600 # Keep logs for 1 hour + backoffLimit: 3 + template: + metadata: + labels: + app: stalwart-security-fix + spec: + serviceAccountName: stalwart-config-access + restartPolicy: OnFailure + containers: + - name: fix-security + image: bitnami/kubectl:latest + command: + - /bin/bash + - -c + - | + set -e + echo "=== Stalwart HTTP Listener Security Fix ===" + echo "Configuring Stalwart to allow internal cluster IPs for the HTTP listener" + echo "" + + # Wait for Stalwart pod to be ready + echo "Waiting for stalwart-0 pod to be ready..." + kubectl wait --for=condition=ready pod/stalwart-0 -n stalwart --timeout=180s + + # Get admin credentials + echo "Retrieving admin credentials..." + ADMIN_EMAIL=$(kubectl get secret stalwart-admin-credentials -n stalwart -o jsonpath='{.data.admin-email}' | base64 -d) + ADMIN_PASSWORD=$(kubectl get secret stalwart-admin-credentials -n stalwart -o jsonpath='{.data.admin-password}' | base64 -d) + + echo "Admin email: $ADMIN_EMAIL" + + # Use kubectl exec to access Stalwart's admin API from localhost + # The HTTP listener allows localhost connections even when blocking other IPs + echo "" + echo "Accessing Stalwart admin API via kubectl exec..." + + # Test API access first + echo "Testing API connectivity..." + kubectl exec -n stalwart stalwart-0 -- curl -s -u "$ADMIN_EMAIL:$ADMIN_PASSWORD" \ + http://localhost:8080/healthz/live + + # Note: The actual API endpoint structure for v0.16.11 may vary + # The web UI uses a REST API, but the exact endpoints for security config + # need to be determined from the Stalwart documentation or by inspecting + # the web UI's network traffic. + + echo "" + echo "✅ Successfully connected to Stalwart API" + echo "" + echo "IMPORTANT: This Job demonstrates API connectivity." + echo "The actual security configuration change requires:" + echo "1. Identifying the correct API endpoint for security settings" + echo "2. Sending the appropriate PUT/POST request to allow cluster IPs" + echo "" + echo "Recommended manual fix:" + echo "1. Temporarily port-forward: kubectl port-forward -n stalwart svc/stalwart-http 8080:8080" + echo "2. Access https://mail.basicstack.de from your browser" + echo "3. Login with admin credentials" + echo "4. Navigate to Settings > Security" + echo "5. Disable IP filtering for the HTTP listener or add 10.244.0.0/16 to allowed IPs" + + exit 0 diff --git a/apps/stalwart/stalwart-config.yaml b/apps/stalwart/stalwart-config.yaml new file mode 100644 index 0000000..d3ff5ed --- /dev/null +++ b/apps/stalwart/stalwart-config.yaml @@ -0,0 +1,66 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: stalwart-config + namespace: stalwart +data: + stalwart.toml: | + # + # Stalwart Mail Server Configuration + # + + [store] + data = "rocksdb" + + [store.rocksdb] + type = "rocksdb" + path = "/var/lib/stalwart" + + # + # Server Configuration + # + + [server] + hostname = "mail.basicstack.de" + + # HTTP Listener (Web UI and API) + [server.listener.http] + bind = ["0.0.0.0:8080"] + protocol = "http" + + # Security: Allow internal cluster IPs for Traefik ingress + # Pod network CIDR: 10.244.0.0/16 + [server.listener.http.security] + allowed-ips = ["10.244.0.0/16", "127.0.0.1/32"] + + # SMTP Listener (Port 25) + [server.listener.smtp] + bind = ["0.0.0.0:25"] + protocol = "smtp" + + # Submission Listener (Port 587 with STARTTLS) + [server.listener.submission] + bind = ["0.0.0.0:587"] + protocol = "smtp" + + # IMAPS Listener (Port 993 with TLS) + [server.listener.imaps] + bind = ["0.0.0.0:993"] + protocol = "imap" + tls.implicit = true + + # + # TLS Configuration + # + + [server.tls] + certificate = "file:///etc/stalwart/certs/tls.crt" + private-key = "file:///etc/stalwart/certs/tls.key" + + # + # Logging + # + + [tracing.level] + default = "info" diff --git a/apps/stalwart/stalwart-fresh-deployment.yaml b/apps/stalwart/stalwart-fresh-deployment.yaml index 9db8be6..220edfa 100644 --- a/apps/stalwart/stalwart-fresh-deployment.yaml +++ b/apps/stalwart/stalwart-fresh-deployment.yaml @@ -98,13 +98,20 @@ spec: - sh - -c - | - cp /tmp/bootstrap-config/* /etc/stalwart/ && \ - echo "Config files copied successfully:" && \ + set -e + echo "Copying bootstrap config..." + cp /tmp/bootstrap-config/* /etc/stalwart/ + echo "Copying stalwart.toml..." + cp /tmp/stalwart-config/stalwart.toml /etc/stalwart/stalwart.toml + echo "Config files copied successfully:" ls -la /etc/stalwart/ volumeMounts: - name: bootstrap-config-source mountPath: /tmp/bootstrap-config readOnly: true + - name: stalwart-config-source + mountPath: /tmp/stalwart-config + readOnly: true - name: config mountPath: /etc/stalwart - name: fix-permissions @@ -196,10 +203,13 @@ spec: # emptyDir for config files (populated by copy-config init-container) - name: config emptyDir: {} - # ConfigMap source for init-container (no longer mounted directly via subPath) + # ConfigMap sources for init-container (no longer mounted directly via subPath) - name: bootstrap-config-source configMap: name: stalwart-bootstrap-config + - name: stalwart-config-source + configMap: + name: stalwart-config - name: tls-certs secret: secretName: stalwart-tls