feat(stalwart): MTA-STS routes as a maintainable list, in both routing modes
The policy route for mta-sts.<domain> belonged to another repository
(jk-cluster-config) while pointing at this chart's own Service. It moves
here as mtaSts.hosts, a list, so several domains - subdomains included -
are maintainable in one place and in whichever routing mode is active.
THE LIST IS THE SWITCH. No mtaSts.enabled: "enabled with an empty list"
would be a state with no meaning that the chart would then have to catch.
The schema rejects the key outright, so the rule is enforced rather than
documented. Default [] renders nothing in either mode - the defaults and
the unchanged CI fixture both produce a byte-identical helm template
against origin/main.
Entry form {host, tlsSecretName, clusterIssuer?}. The two TLS fields are
read ONLY in Ingress mode; in Gateway mode TLS sits on the listener, so
requiring them there would demand a value that does nothing.
THE PATH IS HARDWIRED. RFC 8461 fixes /.well-known/mta-sts.txt, rendered
with pathType Exact (type: Exact in the route). A values key for a
protocol-mandated path can only ever be set wrong, and wrong is invisible
from this side. Exact also matters in itself: a prefix on these hostnames
would publish the mail server's whole management surface under them.
ONE INGRESS PER CLUSTER ISSUER, NOT ONE OBJECT - and the plan's own three
requirements forced that. cert-manager reads the two fields at different
scopes: cluster-issuer is an ANNOTATION (object-scoped, and absent means
nothing is issued for that object at all), while secretName is
tls-entry-scoped and yields one Certificate per entry. So an object cannot
both request a certificate and leave another host's secret alone, which is
what an empty per-entry issuer has to mean. Measured in jk-cluster:
ingress-shim names the Certificate AFTER THE SECRET and owns it from the
Ingress, so a shared secret must sit in exactly one object (hence grouping
the tls list by secret, reusing rss-bridge.tlsGroups' shape) and a secret
may not span two issuer groups (hence the abort). Objects are named by
issuer, not by index: an index is only free where the object holds no
certificate, which is the precondition homeassistant's route naming states
for itself.
Gateway mode renders ONE HTTPRoute with every hostname - nothing differs
per host there - attached to the same Gateway(s) as the WebUI route.
WHAT THE CHART CANNOT PROMISE, AND THEREFORE NAMES: it registers a route,
it does not create a domain. The domains live in Stalwart's data store, so
an entry whose domain is missing there answers 404 at a correctly routed
endpoint with every object green - and MTA-STS failures are silent on the
SENDER's side, which delivers in the clear and logs nothing here. Same for
the _mta-sts.<domain> TXT record and the policy's content. The subdomain
trap is documented too: mta-sts.sub.<domain> is one label below
*.<domain>, so a parent wildcard certificate does not cover it.
Not folded into certificate.extraDnsNames deliberately - that would hang
the MAIL certificate on this list, letting a bad policy entry take STARTTLS
and IMAPS with it.
Minor, additive, empty default: 1.2.0+upv0.16.25 -> 1.3.0+upv0.16.25.
CI: ci/httproute/stalwart.yaml enters all three shapes of the entry form
(with issuer, without, and a subdomain), taking the HTTPRoute render test
from 56 assertions over 21 charts to 58 over 21 - stalwart from 2 to 4.
The Ingress half cannot be covered there, since an overlay's whole job is
to switch the mode off, so ci/stalwart/test-values.yaml carries two
entries and the install-test has a real API server accept them.
Fixes #123
This commit is contained in:
10 files changed
+677
-9
No files matched your search
@@ -63,3 +63,88 @@ spec:
|
||||
- {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
|
||||
secretName: tls-{{ include "stalwart.fullname" . }}-ingress
|
||||
{{- end }}
|
||||
{{- /*
|
||||
MTA-STS (Gitea #123) - the policy route, in Ingress mode.
|
||||
|
||||
ONE OBJECT PER clusterIssuer, one rule per host, tls grouped by secret. The
|
||||
whole reasoning for that shape, including the cert-manager measurement it rests
|
||||
on, is in stalwart.mtaSts.ingressGroups in _helpers.tpl; the short version is
|
||||
that the issuer is an object-level ANNOTATION while the secret is a per-tls-entry
|
||||
field, so an object cannot both ask cert-manager for one certificate and leave
|
||||
another alone.
|
||||
|
||||
GATED ON ingress.enabled, not on a switch of its own. mtaSts.hosts is the
|
||||
switch for the feature (empty list = nothing rendered, in either mode) and
|
||||
ingress.enabled is the switch for the MODE - so these objects appear exactly
|
||||
when this chart is publishing over an Ingress AND the list is non-empty. The
|
||||
mutual exclusion of the two modes is enforced once, in stalwart.httproute.yaml,
|
||||
and it is unconditional on mtaSts: a fail anywhere aborts the whole render.
|
||||
|
||||
WHAT THIS DELIBERATELY DOES NOT CARRY: ingress.annotations. Those belong to the
|
||||
WebUI/JMAP host - on the live installation they are its CORS configuration for
|
||||
the bulwark webmail - and a policy file served to sending MTAs has no use for
|
||||
them. Copying them here would put a CORS allow-origin on an endpoint no browser
|
||||
ever calls, and would do it silently.
|
||||
|
||||
AND THE LIMIT THAT NO OBJECT HERE CAN FIX: this chart registers a ROUTE, it
|
||||
does not create a DOMAIN. Whether Stalwart answers for mta-sts.<domain> depends
|
||||
on <domain> existing in its data store, which is not in any chart value (see
|
||||
values.yaml, line 1 onwards, and the mtaSts block).
|
||||
*/ -}}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- $mtaSts := fromYaml (include "stalwart.mtaSts.ingressGroups" .) }}
|
||||
{{- if $mtaSts.order }}
|
||||
{{- $group := include "stalwart.ingressNodeGroup" . }}
|
||||
{{- $nodes := fromYaml (include "stalwart.nodes" .) }}
|
||||
{{- $spec := index $nodes $group }}
|
||||
{{- if not (has "http" $spec.exposedPorts) }}
|
||||
{{- fail (printf "ingress.nodeGroup is %q, but nodes.%s.exposedPorts does not include \"http\" - there would be no Service for the MTA-STS Ingress to point at" $group $group) }}
|
||||
{{- end }}
|
||||
{{- $service := printf "%s-%s-http-service" (include "stalwart.fullname" .) $group }}
|
||||
{{- $path := include "stalwart.mtaSts.path" . }}
|
||||
{{- range $issuer := $mtaSts.order }}
|
||||
{{- $entry := index $mtaSts.groups $issuer }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "stalwart.mtaSts.ingressName" (dict "root" $ "issuer" $issuer) }}
|
||||
labels:
|
||||
app: {{ include "stalwart.fullname" $ }}
|
||||
{{- if $issuer }}
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ $issuer }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ $.Values.ingress.className }}
|
||||
rules:
|
||||
{{- range $host := $entry.hosts }}
|
||||
- host: {{ $host }}
|
||||
http:
|
||||
paths:
|
||||
{{- /*
|
||||
Exact, not Prefix, and the same per-group http Service the WebUI
|
||||
rule above names - by PORT NAME, which an Ingress backend can do
|
||||
and a Gateway backendRef cannot. RFC 8461 serves exactly one file
|
||||
at exactly one path; a Prefix here would hand this host's whole
|
||||
URL space to the mail server's management surface.
|
||||
*/}}
|
||||
- backend:
|
||||
service:
|
||||
name: {{ $service }}
|
||||
port:
|
||||
name: http
|
||||
path: {{ $path }}
|
||||
pathType: Exact
|
||||
{{- end }}
|
||||
tls:
|
||||
{{- range $secret := $entry.tlsOrder }}
|
||||
- hosts:
|
||||
{{- range $host := index $entry.tls $secret }}
|
||||
- {{ $host }}
|
||||
{{- end }}
|
||||
secretName: {{ $secret }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
Reference in new issue
Block a user