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
155 lines
7.5 KiB
YAML
155 lines
7.5 KiB
YAML
# CI values for the stalwart install-test.
|
|
#
|
|
# WHAT THIS TEST CAN AND CANNOT PROVE — read this before trusting a green run.
|
|
#
|
|
# It CANNOT prove a working mail server, and no install-test here could. Since
|
|
# v0.16 every listener, the blob store, the in-memory store, the TLS object and
|
|
# the cluster roles live in Stalwart's DATABASE and are created by a human in
|
|
# the WebUI. Against an empty database there is no SMTP service to test, so a
|
|
# green run says nothing about mail.
|
|
#
|
|
# What it DOES prove is the part that is the chart's own responsibility, and the
|
|
# first item is the reason this chart is tested rather than excluded:
|
|
#
|
|
# 1. THE CONTAINER EXECS AT ALL. The image carries
|
|
# "setcap cap_net_bind_service=+ep" on its binary, which makes the kernel
|
|
# fail execve itself when the capability is not in the bounding set. The
|
|
# first draft of this chart had capabilities.drop: [ALL] with nothing
|
|
# added and would have failed with "operation not permitted" in every
|
|
# install - caught by reading the Dockerfile, but exactly the class of
|
|
# failure an install-test exists to catch rather than reason about.
|
|
# 2. readOnlyRootFilesystem: true holds with the two image volumes plus /tmp
|
|
# as emptyDirs, and the init container really can write config.json into
|
|
# the /etc/stalwart emptyDir.
|
|
# 3. The rendered config.json parses AS STALWART READS IT, not just as JSON:
|
|
# the server opens the data store, which means "@type": "PostgreSql", the
|
|
# integer-millisecond timeout and the authSecret EnvironmentVariable
|
|
# indirection are all accepted. This is the one check that the password
|
|
# really arrives without ever being in the ConfigMap.
|
|
# 4. The Secret key names match what the templates reference, the ConfigMap
|
|
# mount works, the NodePorts can be allocated, the resource quantities are
|
|
# valid, the schema accepts a realistic (non-default) values file, and the
|
|
# NetworkPolicy objects are accepted without cutting off the probes.
|
|
#
|
|
# RECOVERY MODE IS ON, and that is not a shortcut - it is the only mode that can
|
|
# work here. The bootstrap wizard appears only when config.json is ABSENT, and
|
|
# this chart always renders one; upstream's documented path for a templated
|
|
# config.json is to start once with STALWART_RECOVERY_MODE=1. Recovery mode
|
|
# brings up the management HTTP endpoint and keeps the mail ports closed, which
|
|
# is precisely the reachable surface the probes use.
|
|
#
|
|
# THE ONE THING THIS FILE COULD NOT BE VERIFIED AGAINST: whether /healthz/live
|
|
# and /healthz/ready are served on the RECOVERY listener. The source shows them
|
|
# handled in the general HTTP request path with no auth and no network
|
|
# restriction, but recovery mode binds a single listener and skips most config
|
|
# parsing, and there is no cluster in the authoring environment to try it on.
|
|
# If the install-test goes red with the pods never becoming Ready, that is the
|
|
# reason, and the fix is either an exec probe or an entry in
|
|
# ci/excluded-charts.txt - see the PR for #65.
|
|
bootstrap:
|
|
enabled: true
|
|
logLevel: debug
|
|
|
|
dataStore:
|
|
postgres:
|
|
host: postgres
|
|
database: stalwart
|
|
username: stalwart
|
|
tls:
|
|
# OFF, against the chart's own default of true. The throwaway Postgres in
|
|
# fixtures.yaml serves no TLS, and the alternative - allowInvalidCerts -
|
|
# would be worse: it would pass whether or not TLS worked, so a chart
|
|
# change that broke the TLS path would still go green. A deviation that
|
|
# fails loudly beats one that hides.
|
|
enable: false
|
|
allowInvalidCerts: false
|
|
|
|
secrets:
|
|
# Materialized by ci/stalwart/fixtures.yaml as ci-stalwart-datastore-secret.
|
|
dataStoreSecretRef: op://ci/stalwart-datastore
|
|
# Both deliberately unset: neither is needed to bring a pod up, and leaving
|
|
# them out exercises the conditional env and OnePasswordItem blocks in the
|
|
# "absent" direction, which is the shape a minimal install really has.
|
|
recoveryAdminSecretRef: null
|
|
s3SecretRef: null
|
|
|
|
ingress:
|
|
# Left ON although no ingress controller answers in CI: the point is that the
|
|
# object is accepted and that the per-group http Service it names actually
|
|
# exists - a selector typo there is otherwise invisible until someone opens
|
|
# the URL.
|
|
enabled: true
|
|
domain: stalwart.ci.example.com
|
|
clusterIssuer: ci-selfsigned
|
|
nodeGroup: full
|
|
|
|
# MTA-STS policy routes (Gitea #123). Set HERE and not only in the render
|
|
# overlay, because this is the ONLY stage that puts the INGRESS half of the
|
|
# feature in front of a real API server: ci/httproute/stalwart.yaml switches
|
|
# the chart into Gateway mode, so it never renders these objects at all.
|
|
#
|
|
# WHAT THIS ADDS OVER THE RENDER TEST: that two Ingress objects whose names
|
|
# differ only by the issuer suffix are both accepted, that a `pathType: Exact`
|
|
# on `/.well-known/mta-sts.txt` is valid, and that a tls: list grouping several
|
|
# hosts under one secret is accepted as written. None of that is a template
|
|
# question any more once the API server has taken it.
|
|
#
|
|
# THE TWO ENTRIES ARE TWO OBJECTS, and that is the point of the pair: the first
|
|
# carries a clusterIssuer and gets the cert-manager annotation, the second has
|
|
# none and gets no annotation - which cannot be one object, because the
|
|
# annotation is object-scoped. The second is also a SUBDOMAIN, one label deeper
|
|
# than the first, carrying the case a parent wildcard certificate would not
|
|
# cover.
|
|
#
|
|
# NOTHING ISSUES EITHER CERTIFICATE here: cert-manager is not installed in the
|
|
# k3d cluster, so the annotation is inert and the named Secrets never exist.
|
|
# That is the same deliberate trade as certificate: below - the objects are
|
|
# asserted, the issuance is not, and no Secret is needed for the install to
|
|
# reach Ready because nothing mounts these.
|
|
mtaSts:
|
|
hosts:
|
|
- host: mta-sts.ci.example.com
|
|
tlsSecretName: tls-mta-sts-ci-example-com-ingress
|
|
clusterIssuer: ci-selfsigned
|
|
- host: mta-sts.sub.ci.example.com
|
|
tlsSecretName: tls-mta-sts-sub-ci-example-com-ingress
|
|
|
|
certificate:
|
|
# On, against a STUB CRD (fixtures.yaml). Nothing issues the Secret it names,
|
|
# which is the interesting case: the volume is mounted optional, so the pods
|
|
# must start without it. That is what every real first install looks like
|
|
# before cert-manager has answered the challenge.
|
|
enabled: true
|
|
clusterIssuer: ci-selfsigned
|
|
|
|
networkPolicy:
|
|
# Left ON. probeSources stays empty, so the http rule renders without a
|
|
# "from" selector - the configuration this chart ships. Whether the policy
|
|
# actually FILTERS is a property of the cluster's CNI and is not claimed by
|
|
# this test; what is claimed is that the objects are accepted and that they
|
|
# do not keep the kubelet's probes from working.
|
|
enabled: true
|
|
|
|
nodes:
|
|
full:
|
|
replicas: 1
|
|
exposedPorts: [smtp, submissions, imaps, http]
|
|
resources:
|
|
# Small, to stay inside the runner's budget, while still exercising the
|
|
# limit derivation: 256Mi x limitFactor 3 = 768Mi.
|
|
requests:
|
|
cpu: 100m
|
|
memory: 256Mi
|
|
ephemeral-storage: 64Mi
|
|
mx:
|
|
# Stays at 0, which is also the chart default. Two reasons rather than one:
|
|
# a second node would need an in-memory store (the render aborts without
|
|
# one), and the "mx" ClusterRole does not exist in an empty database, so a
|
|
# running mx pod would fail-open into a second full node and the two would
|
|
# fight over the same NodePort-backed SMTP Service. The StatefulSet is
|
|
# still created and the harness still waits on it - a 0-replica rollout
|
|
# completes immediately, so this covers the explicit-zero path of the
|
|
# replicas helper.
|
|
replicas: 0
|
|
exposedPorts: [smtp, http]
|