checkpoint: stalwart deploy, beam-design, migration scripts, config tweaks
Stalwart + Bulwark mail server deployment with OIDC, TLS cert, vault secrets. Beam design service. Pingora config cleanup. SeaweedFS replication fix. Kratos values tweak. Migration scripts for mbox/messages /calendars from La Suite to Stalwart.
This commit is contained in:
70
base/stalwart/bulwark-deployment.yaml
Normal file
70
base/stalwart/bulwark-deployment.yaml
Normal file
@@ -0,0 +1,70 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bulwark
|
||||
namespace: stalwart
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bulwark
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bulwark
|
||||
spec:
|
||||
containers:
|
||||
- name: bulwark
|
||||
image: src.DOMAIN_SUFFIX/studio/bulwark:latest
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
env:
|
||||
- name: JMAP_SERVER_URL
|
||||
value: https://mail.DOMAIN_SUFFIX
|
||||
- name: OAUTH_ENABLED
|
||||
value: "true"
|
||||
- name: OAUTH_ONLY
|
||||
value: "true"
|
||||
- name: LOG_LEVEL
|
||||
value: "debug"
|
||||
- name: OAUTH_SCOPES
|
||||
value: "openid email profile offline_access"
|
||||
- name: COOKIE_SECURE
|
||||
value: "false"
|
||||
- name: OAUTH_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oidc-bulwark
|
||||
key: CLIENT_ID
|
||||
- name: OAUTH_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oidc-bulwark
|
||||
key: CLIENT_SECRET
|
||||
- name: OAUTH_ISSUER_URL
|
||||
value: https://auth.DOMAIN_SUFFIX
|
||||
- name: SESSION_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: stalwart-app-secrets
|
||||
key: admin-password
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
memory: 128Mi
|
||||
cpu: 50m
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: 500m
|
||||
12
base/stalwart/bulwark-service.yaml
Normal file
12
base/stalwart/bulwark-service.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bulwark
|
||||
namespace: stalwart
|
||||
spec:
|
||||
selector:
|
||||
app: bulwark
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
13
base/stalwart/certificate.yaml
Normal file
13
base/stalwart/certificate.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: stalwart-tls
|
||||
namespace: stalwart
|
||||
spec:
|
||||
secretName: stalwart-tls
|
||||
issuerRef:
|
||||
name: letsencrypt-production
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- "mail.DOMAIN_SUFFIX"
|
||||
- "cal.DOMAIN_SUFFIX"
|
||||
6
base/stalwart/namespace.yaml
Normal file
6
base/stalwart/namespace.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: stalwart
|
||||
labels:
|
||||
linkerd.io/inject: enabled
|
||||
30
base/stalwart/oidc-client-bulwark.yaml
Normal file
30
base/stalwart/oidc-client-bulwark.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
# Bulwark webmail OIDC client — authenticates directly with Hydra.
|
||||
# Hydra Maester creates K8s Secret "oidc-bulwark" with CLIENT_ID/CLIENT_SECRET.
|
||||
# DOMAIN_SUFFIX is replaced by sed at deploy time.
|
||||
apiVersion: hydra.ory.sh/v1alpha1
|
||||
kind: OAuth2Client
|
||||
metadata:
|
||||
name: bulwark
|
||||
namespace: stalwart
|
||||
spec:
|
||||
clientName: Webmail
|
||||
grantTypes:
|
||||
- authorization_code
|
||||
- refresh_token
|
||||
responseTypes:
|
||||
- code
|
||||
scope: openid email profile offline_access
|
||||
redirectUris:
|
||||
- https://mail.DOMAIN_SUFFIX/en/auth/callback
|
||||
- https://mail.DOMAIN_SUFFIX/auth/callback
|
||||
- https://mail.DOMAIN_SUFFIX/api/auth/callback
|
||||
postLogoutRedirectUris:
|
||||
- https://mail.DOMAIN_SUFFIX
|
||||
tokenEndpointAuthMethod: client_secret_post
|
||||
secretName: oidc-bulwark
|
||||
skipConsent: true
|
||||
tokenLifespans:
|
||||
authorization_code_grant_access_token_lifespan: 24h
|
||||
authorization_code_grant_refresh_token_lifespan: 720h
|
||||
refresh_token_grant_access_token_lifespan: 24h
|
||||
refresh_token_grant_refresh_token_lifespan: 720h
|
||||
24
base/stalwart/oidc-client.yaml
Normal file
24
base/stalwart/oidc-client.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
# Stalwart OIDC client — registered with Hydra for SSO login.
|
||||
# Hydra Maester creates K8s Secret "oidc-stalwart" in the stalwart namespace
|
||||
# with CLIENT_ID and CLIENT_SECRET keys.
|
||||
# DOMAIN_SUFFIX is replaced by sed at deploy time.
|
||||
apiVersion: hydra.ory.sh/v1alpha1
|
||||
kind: OAuth2Client
|
||||
metadata:
|
||||
name: stalwart
|
||||
namespace: stalwart
|
||||
spec:
|
||||
clientName: Mail
|
||||
grantTypes:
|
||||
- authorization_code
|
||||
- refresh_token
|
||||
responseTypes:
|
||||
- code
|
||||
scope: openid email profile
|
||||
redirectUris:
|
||||
- https://mail.DOMAIN_SUFFIX/authorize/code
|
||||
postLogoutRedirectUris:
|
||||
- https://mail.DOMAIN_SUFFIX
|
||||
tokenEndpointAuthMethod: client_secret_post
|
||||
secretName: oidc-stalwart
|
||||
skipConsent: true
|
||||
148
base/stalwart/stalwart-config.yaml
Normal file
148
base/stalwart/stalwart-config.yaml
Normal file
@@ -0,0 +1,148 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: stalwart-config
|
||||
namespace: stalwart
|
||||
data:
|
||||
# DOMAIN_SUFFIX is replaced by sed at deploy time.
|
||||
# Secrets (%{env:VAR}%) are injected via environment variables in the Deployment.
|
||||
#
|
||||
# Only LOCAL keys belong in this file (store.*, storage.*, server.*, directory.*,
|
||||
# tracer.*, cluster.*, certificate.*, authentication.fallback-admin.*).
|
||||
# Everything else (OIDC, DKIM, spam, etc.) is configured via the Stalwart
|
||||
# web admin UI and stored in the database.
|
||||
config.toml: |
|
||||
# Force http.url to be read from this file (not the database).
|
||||
config.local-keys.0000 = "store.*"
|
||||
config.local-keys.0001 = "directory.*"
|
||||
config.local-keys.0002 = "tracer.*"
|
||||
config.local-keys.0003 = "server.*"
|
||||
config.local-keys.0004 = "!server.blocked-ip.*"
|
||||
config.local-keys.0005 = "!server.allowed-ip.*"
|
||||
config.local-keys.0006 = "authentication.fallback-admin.*"
|
||||
config.local-keys.0007 = "cluster.*"
|
||||
config.local-keys.0008 = "config.local-keys.*"
|
||||
config.local-keys.0009 = "storage.*"
|
||||
config.local-keys.0010 = "certificate.*"
|
||||
config.local-keys.0011 = "http.url"
|
||||
config.local-keys.0012 = "http.use-x-forwarded"
|
||||
|
||||
# Expression-quoted public URL (inner single quotes required).
|
||||
http.url = "'https://mail.DOMAIN_SUFFIX'"
|
||||
http.use-x-forwarded = true
|
||||
|
||||
[server]
|
||||
hostname = "mail.DOMAIN_SUFFIX"
|
||||
max-connections = 1024
|
||||
|
||||
[server.listener."smtp"]
|
||||
bind = ["0.0.0.0:25"]
|
||||
protocol = "smtp"
|
||||
|
||||
[server.listener."submission"]
|
||||
bind = ["0.0.0.0:587"]
|
||||
protocol = "smtp"
|
||||
tls.implicit = false
|
||||
|
||||
[server.listener."smtps"]
|
||||
bind = ["0.0.0.0:465"]
|
||||
protocol = "smtp"
|
||||
tls.implicit = true
|
||||
|
||||
[server.listener."imap"]
|
||||
bind = ["0.0.0.0:143"]
|
||||
protocol = "imap"
|
||||
tls.implicit = false
|
||||
|
||||
[server.listener."imaps"]
|
||||
bind = ["0.0.0.0:993"]
|
||||
protocol = "imap"
|
||||
tls.implicit = true
|
||||
|
||||
[server.listener."jmap"]
|
||||
bind = ["0.0.0.0:8080"]
|
||||
protocol = "http"
|
||||
|
||||
[server.listener."managesieve"]
|
||||
bind = ["0.0.0.0:4190"]
|
||||
protocol = "managesieve"
|
||||
|
||||
[certificate."default"]
|
||||
cert = "%{file:/etc/stalwart-tls/tls.crt}%"
|
||||
private-key = "%{file:/etc/stalwart-tls/tls.key}%"
|
||||
|
||||
# ── Storage backends ─────────────────────────────────────────────────────
|
||||
|
||||
[store."postgresql"]
|
||||
type = "postgresql"
|
||||
host = "postgres-rw.data.svc.cluster.local"
|
||||
port = 5432
|
||||
database = "stalwart_db"
|
||||
user = "stalwart"
|
||||
password = "%{env:DB_PASSWORD}%"
|
||||
timeout = "15s"
|
||||
|
||||
[store."postgresql".pool]
|
||||
max-connections = 20
|
||||
|
||||
[store."s3"]
|
||||
type = "s3"
|
||||
bucket = "sunbeam-stalwart"
|
||||
endpoint = "http://seaweedfs-filer.storage.svc.cluster.local:8333"
|
||||
region = "us-east-1"
|
||||
access-key = "%{env:S3_ACCESS_KEY}%"
|
||||
secret-key = "%{env:S3_SECRET_KEY}%"
|
||||
key-prefix = "v1/"
|
||||
timeout = "60s"
|
||||
|
||||
[store."opensearch"]
|
||||
type = "elasticsearch"
|
||||
url = "http://opensearch.data.svc.cluster.local:9200"
|
||||
index = "stalwart"
|
||||
|
||||
[store."redis"]
|
||||
type = "redis"
|
||||
urls = ["redis://valkey.data.svc.cluster.local:6379/7"]
|
||||
|
||||
# ── Storage role assignments ─────────────────────────────────────────────
|
||||
|
||||
[storage]
|
||||
data = "postgresql"
|
||||
blob = "postgresql"
|
||||
fts = "opensearch"
|
||||
lookup = "redis"
|
||||
directory = "hydra"
|
||||
|
||||
# ── Directories (user stores) ──────────────────────────────────────────
|
||||
|
||||
# Internal directory for locally-managed accounts (admin, service accounts).
|
||||
[directory."internal"]
|
||||
type = "internal"
|
||||
store = "postgresql"
|
||||
|
||||
# OIDC directory — validates Hydra-issued bearer tokens via userinfo.
|
||||
# When a JMAP client presents a Bearer token, Stalwart calls Hydra's
|
||||
# userinfo endpoint to map it to a user identity.
|
||||
[directory."hydra"]
|
||||
type = "oidc"
|
||||
timeout = "15s"
|
||||
endpoint.url = "http://hydra-public.ory.svc.cluster.local:4444/userinfo"
|
||||
endpoint.method = "userinfo"
|
||||
fields.email = "email"
|
||||
fields.username = "email"
|
||||
fields.full-name = "name"
|
||||
|
||||
# ── Authentication ───────────────────────────────────────────────────────
|
||||
|
||||
[authentication.fallback-admin]
|
||||
user = "admin"
|
||||
secret = "%{env:ADMIN_PASSWORD}%"
|
||||
|
||||
# ── Logging ──────────────────────────────────────────────────────────────
|
||||
|
||||
[tracer."stdout"]
|
||||
type = "stdout"
|
||||
level = "info"
|
||||
ansi = false
|
||||
enable = true
|
||||
|
||||
103
base/stalwart/stalwart-deployment.yaml
Normal file
103
base/stalwart/stalwart-deployment.yaml
Normal file
@@ -0,0 +1,103 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: stalwart
|
||||
namespace: stalwart
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: stalwart
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: stalwart
|
||||
spec:
|
||||
containers:
|
||||
- name: stalwart
|
||||
image: stalwartlabs/stalwart:v0.15.5
|
||||
ports:
|
||||
- name: smtp
|
||||
containerPort: 25
|
||||
- name: submission
|
||||
containerPort: 587
|
||||
- name: smtps
|
||||
containerPort: 465
|
||||
- name: imap
|
||||
containerPort: 143
|
||||
- name: imaps
|
||||
containerPort: 993
|
||||
- name: managesieve
|
||||
containerPort: 4190
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
env:
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: stalwart-db-credentials
|
||||
key: password
|
||||
- name: S3_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: seaweedfs-s3-credentials
|
||||
key: S3_ACCESS_KEY
|
||||
- name: S3_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: seaweedfs-s3-credentials
|
||||
key: S3_SECRET_KEY
|
||||
- name: ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: stalwart-app-secrets
|
||||
key: admin-password
|
||||
- name: DKIM_PRIVATE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: stalwart-app-secrets
|
||||
key: dkim-private-key
|
||||
- name: OIDC_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oidc-stalwart
|
||||
key: CLIENT_ID
|
||||
- name: OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oidc-stalwart
|
||||
key: CLIENT_SECRET
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /opt/stalwart/etc/config.toml
|
||||
subPath: config.toml
|
||||
readOnly: true
|
||||
- name: tls
|
||||
mountPath: /etc/stalwart-tls
|
||||
readOnly: true
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz/live
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz/ready
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 1Gi
|
||||
cpu: "1"
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: stalwart-config
|
||||
- name: tls
|
||||
secret:
|
||||
secretName: stalwart-tls
|
||||
30
base/stalwart/stalwart-service.yaml
Normal file
30
base/stalwart/stalwart-service.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: stalwart
|
||||
namespace: stalwart
|
||||
spec:
|
||||
selector:
|
||||
app: stalwart
|
||||
ports:
|
||||
- name: smtp
|
||||
port: 25
|
||||
targetPort: 25
|
||||
- name: submission
|
||||
port: 587
|
||||
targetPort: 587
|
||||
- name: smtps
|
||||
port: 465
|
||||
targetPort: 465
|
||||
- name: imap
|
||||
port: 143
|
||||
targetPort: 143
|
||||
- name: imaps
|
||||
port: 993
|
||||
targetPort: 993
|
||||
- name: managesieve
|
||||
port: 4190
|
||||
targetPort: 4190
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
85
base/stalwart/vault-secrets.yaml
Normal file
85
base/stalwart/vault-secrets.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultAuth
|
||||
metadata:
|
||||
name: vso-auth
|
||||
namespace: stalwart
|
||||
spec:
|
||||
method: kubernetes
|
||||
mount: kubernetes
|
||||
kubernetes:
|
||||
role: vso
|
||||
serviceAccount: default
|
||||
---
|
||||
# Stalwart DB credentials from OpenBao database secrets engine (static role, 24h rotation).
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultDynamicSecret
|
||||
metadata:
|
||||
name: stalwart-db-credentials
|
||||
namespace: stalwart
|
||||
spec:
|
||||
vaultAuthRef: vso-auth
|
||||
mount: database
|
||||
path: static-creds/stalwart
|
||||
allowStaticCreds: true
|
||||
refreshAfter: 5m
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: stalwart
|
||||
destination:
|
||||
name: stalwart-db-credentials
|
||||
create: true
|
||||
overwrite: true
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
templates:
|
||||
password:
|
||||
text: "{{ index .Secrets \"password\" }}"
|
||||
---
|
||||
# Stalwart application secrets (admin password, DKIM key) from OpenBao KV.
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: stalwart-app-secrets
|
||||
namespace: stalwart
|
||||
spec:
|
||||
vaultAuthRef: vso-auth
|
||||
mount: secret
|
||||
type: kv-v2
|
||||
path: stalwart
|
||||
refreshAfter: 30s
|
||||
destination:
|
||||
name: stalwart-app-secrets
|
||||
create: true
|
||||
overwrite: true
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
templates:
|
||||
admin-password:
|
||||
text: "{{ index .Secrets \"admin-password\" }}"
|
||||
dkim-private-key:
|
||||
text: "{{ index .Secrets \"dkim-private-key\" }}"
|
||||
---
|
||||
# SeaweedFS S3 credentials (shared — same secret as lasuite namespace).
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: seaweedfs-s3-credentials
|
||||
namespace: stalwart
|
||||
spec:
|
||||
vaultAuthRef: vso-auth
|
||||
mount: secret
|
||||
type: kv-v2
|
||||
path: seaweedfs
|
||||
refreshAfter: 30s
|
||||
destination:
|
||||
name: seaweedfs-s3-credentials
|
||||
create: true
|
||||
overwrite: true
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
templates:
|
||||
S3_ACCESS_KEY:
|
||||
text: "{{ index .Secrets \"access-key\" }}"
|
||||
S3_SECRET_KEY:
|
||||
text: "{{ index .Secrets \"secret-key\" }}"
|
||||
Reference in New Issue
Block a user