Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 17s
Validate Helm Charts / Selector match (pull_request) Successful in 16s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build stalwart (pull_request) Successful in 8s
Validate Helm Charts / Trivy config stalwart (pull_request) Successful in 22s
Install-test / Install-test stalwart (pull_request) Successful in 1m44s
Validate Helm Charts / Install-test (pull_request) Successful in 1m44s
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
151 lines
6.5 KiB
YAML
151 lines
6.5 KiB
YAML
{{- /*
|
|
The Ingress for the WebUI and JMAP only. SMTP, IMAP and ManageSieve are not
|
|
HTTP and cannot pass through it - those reach the pods through the per-port
|
|
NodePort Services.
|
|
|
|
It targets exactly ONE node group (ingress.nodeGroup) through that group's own
|
|
http Service, never a port-label selector: the management surface is not safe
|
|
to spread across a group that may not run the management tasks.
|
|
|
|
TWO TRAPS THAT BELONG TO THIS OBJECT AND ARE NOT SOLVED BY IT:
|
|
|
|
1. The auto-ban sees the INGRESS CONTROLLER's pod IP for every JMAP and WebUI
|
|
request, not the client's. Sooner or later it bans that address and the
|
|
WebUI and JMAP go dark for everyone at once. The PROXY protocol is not the
|
|
remedy for HTTP: the remedy is Stalwart's "forwarded" handling with
|
|
X-Forwarded-For evaluation, plus discarding whatever forwarding headers the
|
|
client brought along. That is a configuration step in the database, and it
|
|
is mandatory rather than optional - the failure arrives weeks later and
|
|
looks like a server problem.
|
|
|
|
2. AFTER BOOTSTRAP, PORT 8080 IS NOT THE ADMIN PORT ANY MORE. Upstream:
|
|
"After the restart, the WebUI is reached at https://<hostname>/admin on the
|
|
configured server hostname, and HTTP access on port 8080 is no longer
|
|
available for administration." So this Ingress only works once an HTTP
|
|
NetworkListener on 8080 exists in the database. Between finishing the setup
|
|
and creating that listener, the Ingress resolves to a port that answers
|
|
nothing.
|
|
*/ -}}
|
|
{{- if .Values.ingress.enabled }}
|
|
{{- $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 Ingress to point at" $group $group) -}}
|
|
{{- end -}}
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: {{ include "stalwart.fullname" . }}-ingress
|
|
labels:
|
|
app: {{ include "stalwart.fullname" . }}
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided (ingress.clusterIssuer)" .Values.ingress.clusterIssuer }}
|
|
{{- with .Values.ingress.annotations }}
|
|
{{- toYaml . | nindent 4 }}
|
|
{{- end }}
|
|
spec:
|
|
ingressClassName: {{ .Values.ingress.className }}
|
|
rules:
|
|
- host: {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
|
|
http:
|
|
paths:
|
|
- backend:
|
|
service:
|
|
# The http Service of the "{{ $group }}" group only.
|
|
name: {{ include "stalwart.fullname" . }}-{{ $group }}-http-service
|
|
port:
|
|
name: http
|
|
path: /
|
|
pathType: Prefix
|
|
tls:
|
|
- hosts:
|
|
- {{ 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 }}
|