Skip to main content

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

VariableDescription
JMAP_SERVER_URLURL 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):

VariableDescription
OAUTH_ENABLEDSet to true to enable
OAUTH_CLIENT_IDOAuth2 client ID
OAUTH_CLIENT_SECRETOAuth2 client secret
OAUTH_ISSUER_URLOIDC issuer URL

Optional: Session security

VariableDescription
SESSION_SECRETSecret key for session encryption
SESSION_SECRET_FILEPath 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:

HeaderDefault value
Strict-Transport-Securitymax-age=63072000; includeSubDomains
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), 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.