feat(infra): deploy Stalwart mail server (internal-only, Tailnet access)

Add Stalwart all-in-one mail server: SMTP (cluster-internal relay for
Mattermost), IMAP + HTTPS admin via Tailscale proxy. RocksDB storage,
self-signed TLS (stalwart-tls secret). No public MX, no outbound SMTP.
SCW TEM relay noted in NetworkPolicy comments for future use.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-12 09:22:27 +01:00
parent 9e8b9422cf
commit 8803648b4a
3 changed files with 232 additions and 0 deletions

View File

@@ -86,6 +86,20 @@ spec:
limits:
cpu: 200m
memory: 128Mi
- name: imap-proxy
image: alpine/socat:latest
args:
- "TCP-LISTEN:993,fork,reuseaddr"
- "TCP:stalwart.foxhunt.svc.cluster.local:993"
ports:
- containerPort: 993
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 50m
memory: 32Mi
- name: tailscale
image: ghcr.io/tailscale/tailscale:latest
env:
@@ -257,6 +271,26 @@ data:
}
}
# Stalwart Admin — mail.fxhnt.ai
server {
listen 443 ssl;
server_name mail.fxhnt.ai;
ssl_certificate /etc/nginx/certs/tls.crt;
ssl_certificate_key /etc/nginx/certs/tls.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass https://stalwart.foxhunt.svc.cluster.local:443;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_ssl_verify off;
}
}
# Mattermost — chat.fxhnt.ai
server {
listen 443 ssl;

View File

@@ -0,0 +1,45 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: stalwart
namespace: foxhunt
labels:
app.kubernetes.io/part-of: foxhunt
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: stalwart
policyTypes:
- Ingress
- Egress
ingress:
# SMTP from cluster (Mattermost internal relay)
- ports:
- port: 25
protocol: TCP
from:
- podSelector:
matchLabels:
app.kubernetes.io/name: mattermost
# IMAPS from Tailscale proxy
- ports:
- port: 993
protocol: TCP
# HTTPS admin panel from Tailscale proxy
- ports:
- port: 443
protocol: TCP
egress:
# DNS
- ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
# No outbound SMTP — internal-only for now.
# To add SCW TEM relay later:
# - ports:
# - port: 465
# protocol: TCP
# - port: 587
# protocol: TCP

View File

@@ -0,0 +1,153 @@
# Internal-only mail server. No public MX, no LoadBalancer.
# SMTP: cluster-internal (Mattermost relay)
# IMAP: via Tailscale proxy (port 993)
# HTTPS admin: via Tailscale proxy (port 443 → mail.fxhnt.ai)
# Outbound: not configured (add SCW TEM relay later if needed)
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: stalwart-data
namespace: foxhunt
labels:
app.kubernetes.io/name: stalwart
app.kubernetes.io/part-of: foxhunt
spec:
accessModes: [ReadWriteOnce]
storageClassName: scw-bssd
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: stalwart-config
namespace: foxhunt
labels:
app.kubernetes.io/name: stalwart
app.kubernetes.io/part-of: foxhunt
data:
config.toml: |
[server]
hostname = "mail.fxhnt.ai"
[server.listener.smtp]
bind = "[::]:25"
protocol = "smtp"
[server.listener.imaptls]
bind = "[::]:993"
protocol = "imap"
tls.implicit = true
[server.listener.https]
bind = "[::]:443"
protocol = "http"
tls.implicit = true
[storage]
data = "rocksdb"
blob = "rocksdb"
fts = "rocksdb"
lookup = "rocksdb"
directory = "internal"
[store.rocksdb]
type = "rocksdb"
path = "/opt/stalwart-mail/data"
[directory.internal]
type = "internal"
store = "rocksdb"
[certificate.default]
cert = "/opt/stalwart-mail/tls/tls.crt"
private-key = "/opt/stalwart-mail/tls/tls.key"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: stalwart
namespace: foxhunt
labels:
app.kubernetes.io/name: stalwart
app.kubernetes.io/part-of: foxhunt
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: stalwart
strategy:
type: Recreate # RWO PVC
template:
metadata:
labels:
app.kubernetes.io/name: stalwart
app.kubernetes.io/part-of: foxhunt
spec:
nodeSelector:
k8s.scaleway.com/pool-name: platform
containers:
- name: stalwart
image: stalwartlabs/mail-server:latest
ports:
- containerPort: 25
name: smtp
- containerPort: 993
name: imaps
- containerPort: 443
name: https
volumeMounts:
- name: stalwart-data
mountPath: /opt/stalwart-mail/data
- name: stalwart-config
mountPath: /opt/stalwart-mail/etc/config.toml
subPath: config.toml
- name: stalwart-tls
mountPath: /opt/stalwart-mail/tls
readOnly: true
readinessProbe:
tcpSocket:
port: 25
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumes:
- name: stalwart-data
persistentVolumeClaim:
claimName: stalwart-data
- name: stalwart-config
configMap:
name: stalwart-config
- name: stalwart-tls
secret:
secretName: stalwart-tls
---
apiVersion: v1
kind: Service
metadata:
name: stalwart
namespace: foxhunt
labels:
app.kubernetes.io/name: stalwart
app.kubernetes.io/part-of: foxhunt
spec:
selector:
app.kubernetes.io/name: stalwart
ports:
- port: 25
targetPort: 25
name: smtp
- port: 993
targetPort: 993
name: imaps
- port: 443
targetPort: 443
name: https