Helm base chart
The base chart deploys the Bulwark Mail application itself: Deployment, Service, ServiceAccount, HorizontalPodAutoscaler, and a Gateway API HTTPRoute. It does not provision secrets or observability — see the platform chart for those.
Values reference: helm/base/values.yaml
Install
helm install bulwark-mail ./helm/base \
--set env[2].value=https://mail.example.com \
--set gateway.hostnames[0]=mail.yourdomain.com
Configuration
Required
| Variable | Description |
|---|---|
JMAP_SERVER_URL | URL of your Stalwart Mail Server |
Set via env:
env:
- name: JMAP_SERVER_URL
value: "https://mail.example.com"
Optional: OAuth2/OIDC
Enable SSO by setting these environment variables (or injecting them via ExternalSecrets — see the platform chart):
| Variable | Description |
|---|---|
OAUTH_ENABLED | Set to true to enable |
OAUTH_CLIENT_ID | OAuth2 client ID |
OAUTH_CLIENT_SECRET | OAuth2 client secret |
OAUTH_ISSUER_URL | OIDC issuer URL |
Optional: Session security
| Variable | Description |
|---|---|
SESSION_SECRET | Secret key for session encryption |
SESSION_SECRET_FILE | Path to a file containing the session secret |
Injecting secrets
Don't put OAUTH_CLIENT_SECRET or SESSION_SECRET in env. Use envFrom to pull from a Secret — typically one generated by the platform chart's ExternalSecrets integration:
envFrom:
- secretRef:
name: bulwark-mail
See the platform chart docs for how the Secret gets populated.
Ingress & Security
The base chart emits a Gateway API HTTPRoute. A parent Gateway (with listeners and TLS) is expected to be provided by the platform — typically the hiroba-gateway chart — so the mail chart only owns the route itself.
Minimum configuration
gateway:
parentRefs:
- name: default-gateway
namespace: gateway-system
sectionName: https # pin to the HTTPS listener
hostnames:
- mail.yourdomain.com
Pin sectionName to an HTTPS listener to avoid silently serving plaintext. HTTP→HTTPS redirect is a listener-level concern and lives on the parent Gateway, not in this chart.
Default security headers
Every rule gets a ResponseHeaderModifier filter prepended with these defaults:
| Header | Default value |
|---|---|
Strict-Transport-Security | max-age=63072000; includeSubDomains |
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
Override the list to customize, or set it to [] to opt out entirely:
gateway:
defaultFilters: []
Per-rule filters still work — defaultFilters are prepended, user filters run after. Gateway API evaluates filters in order, so a later filter can overwrite a default header if needed.
Enabling CSP
CSP is intentionally off by default because webmail breaks under strict policies (inline styles in HTML emails, remote images, OAuth redirects). A commented starter is in values.yaml — copy it into your override and tighten per deployment:
gateway:
defaultFilters:
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
# ... other headers ...
- name: Content-Security-Policy
value: >-
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
Tighten img-src to 'self' data: if you want to block tracking pixels in HTML emails (at the cost of legitimate remote images not rendering).
Custom routing rules
The default catch-all sends all traffic to the webmail service. To split paths (e.g. /api to a different backend) set gateway.rules:
gateway:
rules:
- matches:
- path:
type: PathPrefix
value: /
# backendRefs are injected automatically from service.port
Scaling
Horizontal autoscaling is off by default. Bulwark Mail is stateless, so scaling out is safe:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
PodDisruptionBudget
Once you're running more than one replica, enable a PDB so node drains / cluster upgrades can't take the app fully offline:
podDisruptionBudget:
enabled: true
minAvailable: 1
# maxUnavailable: "25%" # mutually exclusive — set minAvailable: null to use this
The selector reuses app.selectorLabels, so it matches the Deployment's pods automatically — no cross-release label coupling needed.
Probes
Liveness, readiness, and startup probes target /api/health on the container port. Override the defaults in livenessProbe, readinessProbe, and startupProbe if your upstream build exposes a different path.
Writable volumes
The container runs with readOnlyRootFilesystem: true. Three emptyDir volumes are mounted for Next.js runtime needs:
/tmp/app/.next/cache/app/data
Extend extraVolumes / extraVolumeMounts if you need additional writable paths.