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
@@ -756,6 +756,161 @@ httpRoute:
|
||||
# hostname is reachable without the application agreeing that it is its name.
|
||||
hostnames: []
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MTA-STS — the policy route, in BOTH routing modes (Gitea #123)
|
||||
# ---------------------------------------------------------------------------
|
||||
# MTA-STS lets a sending mail server find out, over HTTPS, that this domain
|
||||
# expects TLS. Stalwart serves the policy file itself — this block is only
|
||||
# about ROUTING a sending MTA to it, and the route has to exist in whichever
|
||||
# mode this chart is publishing in.
|
||||
#
|
||||
# WHAT IS RENDERED, AND IT IS NOT THE SAME SHAPE ON BOTH SIDES:
|
||||
#
|
||||
# Ingress mode — ONE Ingress object PER DISTINCT clusterIssuer, each with one
|
||||
# rule per host and its tls: list grouped by secret name.
|
||||
# Several objects because cert-manager's cluster-issuer is an
|
||||
# object-level ANNOTATION while secretName is a per-tls-entry
|
||||
# field, so one object cannot both request a certificate from
|
||||
# issuer X and leave another host's secret alone. The full
|
||||
# argument, with the cert-manager behaviour it was measured
|
||||
# against, is in stalwart.mtaSts.ingressGroups in
|
||||
# _helpers.tpl.
|
||||
# Gateway mode — ONE HTTPRoute with all the hostnames. Nothing differs per
|
||||
# host here (same path, same backend, no filter) and
|
||||
# spec.hostnames is a list, so one object says exactly what
|
||||
# is meant. TLS sits on the Gateway's listener and this chart
|
||||
# renders no certificate for it.
|
||||
#
|
||||
# Both point at the same per-group http Service the WebUI route points at
|
||||
# (ingress.nodeGroup), on the one path RFC 8461 allows.
|
||||
#
|
||||
# THE LIST IS THE SWITCH. There is deliberately no mtaSts.enabled: "enabled
|
||||
# with an empty list" would be a state with no meaning that the chart would
|
||||
# then have to catch. Empty list — the default — renders nothing in either
|
||||
# mode, so this block changes nothing about a running installation until it is
|
||||
# filled.
|
||||
#
|
||||
# THE PATH IS HARDWIRED and is not a value. RFC 8461 fixes it: the policy is
|
||||
# fetched from /.well-known/mta-sts.txt on mta-sts.<domain>, with pathType
|
||||
# Exact (type: Exact in the HTTPRoute) because exactly one file lives there. A
|
||||
# values key for a protocol-mandated path is a key that can only ever be set
|
||||
# wrong — and "wrong" here is invisible from this side, see the limit below.
|
||||
# Exact rather than a prefix also matters in its own right: a prefix on these
|
||||
# hostnames would publish the mail server's whole management surface under
|
||||
# them.
|
||||
#
|
||||
# ------------------------------------------------------------------------
|
||||
# THE LIMIT, AND IT HAS TO BE SAID RATHER THAN SUGGESTED AWAY
|
||||
# ------------------------------------------------------------------------
|
||||
# THIS CHART REGISTERS A ROUTE. IT DOES NOT CREATE A DOMAIN. Read the first
|
||||
# paragraph of this file again: since v0.16 the domains live in Stalwart's DATA
|
||||
# STORE, and this chart never sees them. So whether the server answers for
|
||||
# mta-sts.example.com depends on example.com existing in that data store, which
|
||||
# no value here can arrange.
|
||||
#
|
||||
# An entry without its domain in the data store gives a 404 AT A CORRECTLY
|
||||
# ROUTED ENDPOINT — every Kubernetes object green, the Ingress or route
|
||||
# resolving perfectly, and nothing wrong on this side at all. And the other
|
||||
# half of it is worse: MTA-STS failures are silent ON THE SENDER'S SIDE. A
|
||||
# sending MTA that finds no policy does not report anything here; it simply
|
||||
# falls back to delivering in the clear. So the thing this block exists to
|
||||
# achieve can be absent with no symptom in this cluster whatsoever.
|
||||
#
|
||||
# That is the same genus as the OnePasswordItem limit this chart already
|
||||
# documents: a promise the chart cannot keep, and therefore one it has to name.
|
||||
# Check an entry from outside after adding it:
|
||||
# curl https://mta-sts.<domain>/.well-known/mta-sts.txt
|
||||
#
|
||||
# ------------------------------------------------------------------------
|
||||
# THE SUBDOMAIN TRAP, which is the one that will actually catch somebody
|
||||
# ------------------------------------------------------------------------
|
||||
# mta-sts.sub.example.com is ONE LABEL DEEPER than *.example.com, and a
|
||||
# wildcard certificate matches exactly one label (RFC 6125). So a wildcard for
|
||||
# the parent domain does NOT cover it, and the obvious assumption — "the
|
||||
# wildcard already covers that" — is false.
|
||||
#
|
||||
# With a secret per entry that is solved, which is why the field exists. The
|
||||
# trap is written down anyway because the symptom is a TLS error AT THE SENDING
|
||||
# MTA: nothing in this cluster logs it, the endpoint looks healthy from here,
|
||||
# and mail keeps flowing unencrypted. In Gateway mode the same trap moves to
|
||||
# the Gateway's listener, where this chart cannot help at all — a wildcard
|
||||
# listener for the parent domain will not terminate TLS for a deeper name.
|
||||
#
|
||||
# ------------------------------------------------------------------------
|
||||
# WHY A SECRET PER ENTRY AND NOT certificate.extraDnsNames
|
||||
# ------------------------------------------------------------------------
|
||||
# The alternative was real and was rejected. This chart already has
|
||||
# "Additional DNS names for the mail certificate" (certificate.extraDnsNames,
|
||||
# further up this file), so the mta-sts.* names could have been folded into the
|
||||
# mail certificate instead of carrying their own secrets.
|
||||
#
|
||||
# They are not, because that would hang the MAIL certificate on this list. The
|
||||
# mail certificate is what serves STARTTLS on 25 and implicit TLS on 465/993;
|
||||
# a bad entry here — a typo, a name whose DNS-01 challenge cannot be answered —
|
||||
# would then fail the issuance of the certificate the mail ports depend on.
|
||||
# A policy route that is only a nice-to-have would be able to take SMTP and
|
||||
# IMAP with it. Separate secrets keep the blast radius at the entry.
|
||||
#
|
||||
# ------------------------------------------------------------------------
|
||||
# NOT DONE BY THIS CHART, so it is not mistaken for done
|
||||
# ------------------------------------------------------------------------
|
||||
# - THE DNS RECORD. MTA-STS also needs a TXT record at _mta-sts.<domain>;
|
||||
# without it no sending MTA ever looks for the policy. That is DNS, not
|
||||
# Kubernetes, and nothing here creates it.
|
||||
# - THE POLICY'S CONTENT. version/mode/max_age/mx come from Stalwart's data
|
||||
# store, per domain. The running installation serves mode: testing, which
|
||||
# means sending MTAs enforce nothing — also a data-store setting and not a
|
||||
# chart question.
|
||||
# - REMOVING AN EXISTING OBJECT FROM ANOTHER REPOSITORY. Where a route for
|
||||
# one of these hosts already exists elsewhere (jk-cluster-config does serve
|
||||
# mta-sts.bekemeyer-klein.de from a Fleet release of its own), the order is
|
||||
# REMOVE THERE FIRST, then add the entry here. The other way round,
|
||||
# ingress-nginx's admission webhook rejects the second object on the same
|
||||
# host and path — which is the loud failure, but it is a failure.
|
||||
mtaSts:
|
||||
# Empty by default: no object in either mode, and no value becomes required.
|
||||
#
|
||||
# Entry form, and the two TLS fields apply ONLY IN INGRESS MODE:
|
||||
#
|
||||
# host REQUIRED. The policy hostname, i.e. mta-sts.<domain>.
|
||||
# tlsSecretName REQUIRED IN INGRESS MODE, and with NO DEFAULT on purpose:
|
||||
# the plausible default (this release's own
|
||||
# tls-<release>-<chart>-ingress) is the wrong certificate —
|
||||
# it covers ingress.domain, not this host. Hosts that share
|
||||
# one secret are collected into one tls: entry, so one
|
||||
# certificate with several SANs is asked for once rather
|
||||
# than once per host. Inert in Gateway mode.
|
||||
# clusterIssuer OPTIONAL, and EMPTY MEANS NO ANNOTATION — i.e. "the
|
||||
# certificate for this host comes from somewhere else, do
|
||||
# not ask cert-manager for it". That is what keeps a secret
|
||||
# owned by another release from being fought over, the same
|
||||
# deliberate omission as the qBittorrent object in
|
||||
# freesailarr and two of the four realm ingresses in Gitea
|
||||
# #122. Entries sharing a secret must agree on it (the
|
||||
# render aborts naming both), because cert-manager names the
|
||||
# Certificate after the secret and the issuer is an
|
||||
# object-level annotation. Inert in Gateway mode.
|
||||
#
|
||||
# The chart's own ingress.annotations are NOT copied onto these objects: they
|
||||
# belong to the WebUI/JMAP host (on this installation they are its CORS
|
||||
# configuration), and a policy file served to sending MTAs has no use for
|
||||
# them.
|
||||
#
|
||||
# hosts:
|
||||
# - host: mta-sts.example.com
|
||||
# tlsSecretName: tls-mta-sts-example-com-ingress
|
||||
# clusterIssuer: letsencrypt-issuer
|
||||
# # A deeper name needs its own certificate — the parent's wildcard does
|
||||
# # not reach it. See the subdomain trap above.
|
||||
# - host: mta-sts.sub.example.com
|
||||
# tlsSecretName: tls-mta-sts-sub-example-com-ingress
|
||||
# clusterIssuer: letsencrypt-issuer
|
||||
# # Certificate managed elsewhere: no issuer, so no annotation and no
|
||||
# # Certificate requested by this release.
|
||||
# - host: mta-sts.other.example
|
||||
# tlsSecretName: tls-some-existing-wildcard-ingress
|
||||
hosts: []
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hardening — chart-wide, applied to every node group
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user