Files
helm-charts/matrix-synapse/values.yaml
T
jklein 470b90a393 feat(matrix-synapse): Gateway-API as an alternative to the Ingress
Group 3 of the #114 sweep. TWO HTTPRoutes - and the shape is not the one the
issue described.

#114 says this chart renders its Ingresses in a LOOP OVER SEVERAL APPS. It
does not; that is freesailarr. It renders ONE Ingress object with TWO HOST
rules: the homeserver on ingress.domain at "/", and the delegation nginx on
matrix.delegation.serverUrl at "/.well-known/matrix". An HTTPRoute's
spec.hostnames applies to the WHOLE object, so two hostnames that want
different paths and different backends have to be two objects. That is the
only faithful translation, not a stylistic choice.

httpRoute.delegation.hostnames deliberately does NOT fall back to
httpRoute.hostnames. The server name is what REMOTE homeservers resolve and is
a different thing from the homeserver's own hostname; the migration order -
homeserver host first, server name last - depends on telling them apart, and a
chart-global hostname list covering both would erase the distinction.
httpRoute.delegation.parentRefs falls back to httpRoute.parentRefs, which
matters because the server name is usually an apex domain behind a different
Gateway.

ONE THING THIS MODE IMPROVES, and it is the awkward part of Matrix delegation.
Under ingress-nginx two Ingress objects cannot both claim one hostname - the
admission webhook rejects the second - which is why the delegation rule has to
live inside this chart's single Ingress object even though the apex domain
usually belongs to a website. HTTPRoutes attached to the same listener for the
same hostname are merged by the implementation, longest match winning, so the
/.well-known/matrix route can sit beside whatever owns that apex.

WHAT THIS MODE DROPS, MEASURED ON THE LIVE OBJECT ON 2026-10-12 - and ONE OF
THE TWO IS NOT WHAT IT LOOKS LIKE. The live Ingress carried, all of it
operator-supplied through ingress.annotations (this chart sets none of it):

    nginx.ingress.kubernetes.io/proxy-body-size:    50m
    nginx.org/client-max-body-size:                 50m
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Forwarded-For $remote_addr";
      more_set_headers "X-Forwarded-Proto $scheme";
      more_set_headers "Host $host:$server_port";

1. THE BODY SIZE IS THE REAL LOSS. Synapse's media repository takes uploads;
   its own ceiling is max_upload_size in homeserver.yaml, which this chart
   cannot see because the whole config arrives from 1Password. The effective
   limit is the smaller of the two, and in this mode the proxy half becomes
   the GATEWAY'S default - neither number anyone can point at is necessarily
   the one that decides. No counterpart in the Gateway API, so per decision 2
   of #114 it is documented, not rebuilt. nginx.org/client-max-body-size is
   the same limit spelled for NGINX Inc's controller, a hedge across both
   implementations, and it goes for the same reason. (#114's own measured
   table did not list matrix-synapse under proxy-body-size at all; the live
   object has it.)

2. THE SNIPPET IS NOT LOAD-BEARING, which took checking rather than assuming.
   It looks like what Synapse's x_forwarded setting depends on - client IP for
   rate limiting, and federation. It is not: more_set_headers is the
   headers-more module's OUTPUT-header directive ("Replaces (if any) or adds
   (if not any) the specified output headers"), the request-side directive
   being more_set_input_headers. So those three headers are added to the
   RESPONSE TO THE CLIENT; Synapse never sees them, and what it reads with
   x_forwarded: true is the REQUEST's X-Forwarded-For, which ingress-nginx
   supplies itself. The Gateway API's ResponseHeaderModifier could not rebuild
   it anyway (it knows no nginx variables), and should not: NOTES.txt says in
   as many words not to re-create it and not to read its absence as the cause
   of a rate-limiting or federation problem.

   Stated as what it is - a reading of the directive's documented semantics.
   Nothing here observed a Synapse instance with and without the snippet.

NOTES.txt now asks $published (ingress.enabled OR httpRoute.enabled) wherever
it used to ask ingress.enabled - the server-name/serverUrl mismatch warning,
the "clients work fine" aside and the scaled-to-zero paragraph were all really
asking whether anything publishes the release.

Minor: additive, defaults unchanged. Empty helm template diff against
origin/main with ci/matrix-synapse/test-values.yaml.

Part of #114
2026-10-12 20:55:55 +00:00

288 lines
12 KiB
YAML

# Scale the whole chart down without uninstalling it: scaleDown: true renders
# every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and
# takes precedence over any per-workload replicas value. PVCs, Secrets and
# Services stay in place; scaleDown: false brings the workloads back up.
scaleDown: false
secrets:
# homeserver.yaml is mounted into the Synapse StatefulSet from the Secret the
# 1Password operator materializes from this item.
#
# Known limitation (#82): a change to the item in the vault does NOT restart
# Synapse. Helm cannot hash the content (it never sees it - the operator fills
# the Secret in-cluster after rendering), so the checksum/config mechanism the
# delegation nginx uses does not apply here. The operator's own
# "operator.1password.io/auto-restart" annotation is no help either: it only
# ever restarts Deployments, and this workload is a StatefulSet (verified
# against onepassword-operator 1.12.0). After changing the item, restart
# Synapse by hand:
# kubectl rollout restart statefulset <release>-matrix-synapse-sfs
configSecretRef: null
matrix:
# Replica count for this workload; 0 is allowed and scales it down.
# The chart-global scaleDown flag takes precedence.
replicas: 1
serverName: null
timezone: UTC
# The upstream image (ghcr.io/element-hq/synapse) only drops privileges
# itself (via gosu, driven by UID/GID env vars) when started as root; with
# an explicit non-root runAsUser it skips that step and runs directly as
# that user. Synapse writes to /data and /media_store (both on the PVC) and
# to /tmp (mounted as an emptyDir by the chart), so the root filesystem
# stays read-only. The uid/gid 1000 matches the previous gosu default, so
# existing PVC data stays accessible.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: Always
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
livenessProbe:
httpGet:
path: /health
port: http
scheme: HTTP
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
scheme: HTTP
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# The startup probe gates liveness/readiness. Synapse can take a
# while to start (migrations, cache warmup): budget 10s * 30 = 300s.
startupProbe:
httpGet:
path: /health
port: http
scheme: HTTP
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
# Extra environment variables, appended to the ones the chart always sets.
# Values are rendered through tpl; value, secretKeyRef and configMapKeyRef
# forms are supported.
env: []
resources:
# Limits are optional: memory and ephemeral-storage limits default to
# limitFactor x the request, an explicitly set limit always wins, and a
# CPU limit is only set when configured explicitly under "limits".
limitFactor: 3
requests:
memory: "1Gi"
cpu: "1"
ephemeralStorage: "1Gi"
delegation:
# Replica count for this workload; 0 is allowed and scales it down.
# The chart-global scaleDown flag takes precedence.
replicas: 1
enabled: false
serverUrl: null
# Auxiliary image, pinned independently of appVersion (which tracks
# Synapse). Reviewed alongside every app-version update round.
image:
repository: nginx
tag: 1.30.4
# The delegation nginx runs as the image's unprivileged nginx user
# (uid 101) and listens on the unprivileged port 8080; pid file and temp
# paths are redirected to the /tmp emptyDir by the mounted nginx.conf.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
livenessProbe:
httpGet:
path: /.well-known/matrix/server
port: http
scheme: HTTP
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /.well-known/matrix/server
port: http
scheme: HTTP
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
startupProbe:
httpGet:
path: /.well-known/matrix/server
port: http
scheme: HTTP
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
resources:
# Limits are optional: memory and ephemeral-storage limits default to
# limitFactor x the request, an explicitly set limit always wins, and a
# CPU limit is only set when configured explicitly under "limits".
limitFactor: 3
requests:
memory: "64Mi"
cpu: "125m"
ephemeralStorage: "50Mi"
storage:
amount: 10Gi
storageClass: longhorn-encrypted
ingress:
# Set to false to omit the Ingress entirely. Note that ingress.domain is also
# read by the delegation nginx, which serves it in the /.well-known/matrix/*
# endpoints, so it stays required whenever matrix.delegation.enabled is true.
# Only clusterIssuer becomes unnecessary.
#
# MUTUALLY EXCLUSIVE WITH httpRoute.enabled - the render aborts when both are
# true, and the message names both value paths.
enabled: true
domain: null
clusterIssuer: null
className: nginx
# Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets.
#
# INERT IN GATEWAY MODE - the Gateway API has no annotation channel for
# controller behaviour. For THIS chart that is not a formality: read the
# httpRoute block below for what this installation actually has in here and
# which half of it matters.
annotations: {}
# THE GATEWAY-API ALTERNATIVE TO THE INGRESS (Gitea #114).
#
# Mutually exclusive with ingress.enabled - the render aborts when both are
# true. The chart renders ONLY HTTPRoutes with parentRefs: no Gateway, no
# class, no Certificate, and no implementation-specific policy object. Class
# and certificate sit on the Gateway and its listeners and belong to the
# platform (cluster-config), which is what keeps the Gateway implementation
# replaceable without touching this chart again.
#
# TWO ROUTES, not one. The Ingress is a single object with two HOST rules - the
# homeserver on ingress.domain at "/", and the delegation nginx on
# matrix.delegation.serverUrl at "/.well-known/matrix". An HTTPRoute's
# hostnames apply to the whole object, so two hostnames wanting different paths
# and different backends must be two objects. (#114 describes this chart as
# rendering its Ingresses in a loop over several apps; it does not - that is
# freesailarr.)
#
# ONE THING THIS MODE MAKES EASIER, and it is the awkward part of Matrix
# delegation: the server name is usually an apex domain that already serves a
# website. Under ingress-nginx two Ingress objects cannot both claim one
# hostname - the admission webhook rejects the second - which is why the
# delegation rule has to live inside this chart's single Ingress object.
# HTTPRoutes attached to the same listener for the same hostname are MERGED by
# the implementation, longest match winning, so here the /.well-known/matrix
# route can sit beside whatever owns that apex without either side knowing.
#
# WHAT THIS MODE COSTS, MEASURED ON THE LIVE OBJECT ON 2026-10-12, where the
# Ingress carried - all of it OPERATOR-supplied through ingress.annotations,
# since this chart sets none of it:
#
# nginx.ingress.kubernetes.io/proxy-body-size: 50m
# nginx.org/client-max-body-size: 50m
# nginx.ingress.kubernetes.io/configuration-snippet: |
# more_set_headers "X-Forwarded-For $remote_addr";
# more_set_headers "X-Forwarded-Proto $scheme";
# more_set_headers "Host $host:$server_port";
#
# 1. THE BODY SIZE IS THE REAL LOSS. Synapse's media repository takes uploads
# and its own ceiling is max_upload_size in homeserver.yaml - which this
# chart cannot see, because the whole config comes from 1Password through
# secrets.configSecretRef. The effective limit is the smaller of the two,
# and in this mode the proxy half becomes THE GATEWAY'S default: neither
# number anyone can point at is necessarily the one that decides. The
# Gateway API has no body-size field, so per decision 2 of #114 this is
# documented rather than rebuilt. Set it next to the Gateway, in that
# implementation's own object, and set it to match max_upload_size.
# (nginx.org/client-max-body-size is the same limit spelled for NGINX Inc's
# ingress controller rather than ingress-nginx - a hedge across both - and
# it goes for the same reason.)
#
# 2. THE SNIPPET IS NOT LOAD-BEARING, WHICH TOOK CHECKING. It looks like what
# Synapse's x_forwarded setting depends on - client IP for rate limiting,
# and federation. It is not: more_set_headers is the OUTPUT-header directive
# of headers-more-nginx-module ("Replaces (if any) or adds (if not any) the
# specified output headers"), while the request-side directive is
# more_set_input_headers. These three headers are therefore added to the
# RESPONSE TO THE CLIENT; Synapse never sees them, and what it reads with
# x_forwarded: true is the REQUEST's X-Forwarded-For, which ingress-nginx
# sets itself.
#
# So for the migration: not translatable, and not needed. The Gateway API's
# ResponseHeaderModifier can set static response headers but knows no nginx
# variables ($remote_addr, $scheme, $host), so rebuilding it is impossible
# anyway - and pointless. Do NOT re-create it at the Gateway in the belief
# that Synapse needs it.
#
# Stated as what it is: a reading of the directive's documented semantics.
# Nothing here observed a Synapse instance with and without the snippet.
#
# THE GATEWAY-API CRDs MUST BE PRESENT. The chart cannot detect them
# (.Capabilities.APIVersions is unreliable at template time) and an HTTPRoute
# without them fails at install, so this is a value and not a guess.
httpRoute:
enabled: false
# The Gateway(s) the HOMESERVER route attaches to - a platform object,
# usually in another namespace, whose listener must allow this namespace in
# allowedRoutes. Entries are the Gateway API's ParentReference (name, and
# optionally namespace, sectionName, port, group, kind), passed through
# unchanged:
#
# parentRefs:
# - name: public
# namespace: gateway
# sectionName: https
#
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
# list, because a route without a parent is created, is valid and routes
# nothing, with no event to say so.
parentRefs: []
# Empty means [ingress.domain], so the homeserver hostname has one source of
# truth. Set explicitly only to publish under a different or an additional
# name - which is how a migration from the Ingress runs: the HTTPRoute takes
# a second hostname while the Ingress keeps ingress.domain.
#
# THE DELEGATION HOST IS NOT COVERED BY THIS LIST, deliberately - see below.
hostnames: []
delegation:
# The delegation route's own parent. Empty falls back to
# httpRoute.parentRefs. Worth setting when the SERVER NAME is published by
# a different Gateway than the homeserver host, which is the normal case:
# the server name is an apex domain somebody else's website usually owns.
parentRefs: []
# Empty means [matrix.delegation.serverUrl], and it deliberately does NOT
# fall back to httpRoute.hostnames. The server name is a different thing
# from the homeserver's hostname - it is what REMOTE homeservers resolve -
# and the migration order depends on telling them apart: move the
# homeserver host first, confirm it answers, move the server name last.
# A chart-global hostname list covering both would erase that distinction.
# Set this only when the server name itself has to move.
hostnames: []