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
@@ -163,3 +163,86 @@ spec:
|
||||
- name: {{ include "stalwart.fullname" . }}-{{ $group }}-http-service
|
||||
port: {{ int (index $ports "http").containerPort }}
|
||||
{{- end }}
|
||||
{{- /*
|
||||
MTA-STS (Gitea #123) - the policy route, in Gateway mode.
|
||||
|
||||
ONE OBJECT WITH ALL THE HOSTNAMES, and here that is right where the Ingress
|
||||
side needs several objects. spec.hostnames applies to the whole object and
|
||||
nothing differs per host: same path, same backend, no filter. So one object
|
||||
with a hostname list says exactly what is meant. (The contrast worth knowing:
|
||||
Gitea #122 needs one object per host for keycloak, because its app-root
|
||||
redirect differs per realm and a rule's matches are path-based - there is no
|
||||
way to tell hostnames apart inside one route. And the Ingress side here needs
|
||||
several objects for a third, unrelated reason: the cert-manager annotation is
|
||||
object-scoped. Three different shapes, three different causes.)
|
||||
|
||||
NO TLS HERE, and therefore no use for either TLS field of an entry.
|
||||
tlsSecretName and clusterIssuer are accepted and INERT in this mode - the
|
||||
listener on the Gateway carries the certificate, and this chart renders no
|
||||
certificate for an HTTP hostname in Gateway mode. Which is why tlsSecretName is
|
||||
required only on the Ingress path: a value demanded in a mode where it does
|
||||
nothing is a value that teaches the wrong thing.
|
||||
|
||||
AND THE HOSTNAMES ARE A TRAP WORTH NAMING AT THE LISTENER. mta-sts.sub.domain.tld
|
||||
is one label deeper than *.domain.tld, so a wildcard listener for the parent
|
||||
does NOT terminate TLS for it (RFC 6125: a wildcard matches one label). The
|
||||
Gateway needs a listener that really covers each hostname below, and the
|
||||
symptom of getting it wrong is a TLS error at the SENDING MTA - nothing in this
|
||||
cluster logs it.
|
||||
|
||||
NO SEPARATE parentRefs. The route attaches to the same Gateway(s) as the WebUI
|
||||
route above, which is the honest default: these hostnames point at the same
|
||||
Service in the same namespace. If they ever need a Gateway of their own, the
|
||||
shape to copy is keycloak's httpRoute.admin.parentRefs (Gitea #114, group 3) -
|
||||
an own list falling back to httpRoute.parentRefs - and not a second required
|
||||
value. The abort on an empty httpRoute.parentRefs above covers this object too:
|
||||
it runs whenever httpRoute.enabled is true, before anything here is rendered.
|
||||
*/ -}}
|
||||
{{- if $httpRoute.enabled }}
|
||||
{{- $mtaStsHosts := fromYamlArray (include "stalwart.mtaSts.hosts" .) }}
|
||||
{{- if $mtaStsHosts }}
|
||||
{{- $group := include "stalwart.ingressNodeGroup" . }}
|
||||
{{- $nodes := fromYaml (include "stalwart.nodes" .) }}
|
||||
{{- $ports := fromYaml (include "stalwart.ports" .) }}
|
||||
{{- $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 HTTPRoute to point at" $group $group) }}
|
||||
{{- end }}
|
||||
{{- $parentRefs := $httpRoute.parentRefs | default list }}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ include "stalwart.fullname" . }}-mta-sts-httproute
|
||||
labels:
|
||||
app: {{ include "stalwart.fullname" . }}
|
||||
spec:
|
||||
parentRefs:
|
||||
{{- toYaml $parentRefs | nindent 4 }}
|
||||
hostnames:
|
||||
{{- range $entry := $mtaStsHosts }}
|
||||
- {{ $entry.host }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- /*
|
||||
type: Exact, not PathPrefix - RFC 8461 serves exactly one file at
|
||||
exactly one path, and a prefix here would publish the mail server's
|
||||
whole management surface under these hostnames.
|
||||
|
||||
The port is the Service's port NUMBER, from ports.http.containerPort,
|
||||
exactly as in the WebUI route above: backendRefs takes a PortNumber, so
|
||||
the name "http" the Ingress uses is not available here. Note that this
|
||||
chart's NodePort Services publish a DIFFERENT number
|
||||
(ports.<name>.nodePort) than the container binds, so the wrong one is a
|
||||
plausible mistake - ci/run-httproute-match-test.sh holds this reference
|
||||
against the Services of the same render.
|
||||
*/}}
|
||||
- matches:
|
||||
- path:
|
||||
type: Exact
|
||||
value: {{ include "stalwart.mtaSts.path" . }}
|
||||
backendRefs:
|
||||
- name: {{ include "stalwart.fullname" . }}-{{ $group }}-http-service
|
||||
port: {{ int (index $ports "http").containerPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
Reference in new issue
Block a user