feat(mail): Outlook/Thunderbird autodiscovery over HTTPS
ci / changes (push) Successful in 4s
ci / tc_portal (push) Has been skipped
ci / tc_operator (push) Has been skipped
ci / tc_website (push) Has been skipped
ci / tc_booking (push) Has been skipped
ci / tc_platform_api (push) Successful in 21s
ci / build_portal (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / build_operator (push) Has been skipped
ci / test_platform_api (push) Successful in 33s
ci / build_platform_api (push) Successful in 19s
ci / deploy (push) Failing after 9s
ci / changes (push) Successful in 4s
ci / tc_portal (push) Has been skipped
ci / tc_operator (push) Has been skipped
ci / tc_website (push) Has been skipped
ci / tc_booking (push) Has been skipped
ci / tc_platform_api (push) Successful in 21s
ci / build_portal (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / build_operator (push) Has been skipped
ci / test_platform_api (push) Successful in 33s
ci / build_platform_api (push) Successful in 19s
ci / deploy (push) Failing after 9s
Outlook autodiscovers via POST https://autodiscover.<domain>/autodiscover/ autodiscover.xml and Thunderbird via autoconfig.<domain>/mail/ config-v1.1.xml — Stalwart serves both (verified, answers carry mail.dezky.eu:993/465) but its HTTP listener wasn't reachable from outside (the node's :443 is Traefik's). New exact-path-only Ingress routes JUST those discovery endpoints to host-Stalwart via a selectorless Service + Endpoints on the cni0 gateway; the admin/management surface stays internal, and there's no HTTPS-redirect middleware because Thunderbird probes plain HTTP and Outlook POSTs. Domains page now also lists the autoconfig/autodiscover CNAMEs under the autodiscovery slot (CNAME verified against the mail host; a bare A record warns instead of failing). Customer-domain autodiscovery (per-domain certs + automated Ingress) is a follow-up.
This commit is contained in:
@@ -7,6 +7,7 @@ namespace: dezky-apps
|
|||||||
resources:
|
resources:
|
||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- redirect-middleware.yaml
|
- redirect-middleware.yaml
|
||||||
|
- mail-autodiscovery.yaml
|
||||||
- platform-api-config.yaml
|
- platform-api-config.yaml
|
||||||
- platform-api.yaml
|
- platform-api.yaml
|
||||||
- portal.yaml
|
- portal.yaml
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Mail-client autodiscovery for dezky.eu — routes ONLY the discovery paths
|
||||||
|
# through Traefik to host-Stalwart's HTTP listener (10.42.0.1:8080):
|
||||||
|
#
|
||||||
|
# autodiscover.dezky.eu POST /autodiscover/autodiscover.xml (Outlook)
|
||||||
|
# autoconfig.dezky.eu GET /mail/config-v1.1.xml (Thunderbird)
|
||||||
|
#
|
||||||
|
# Everything else on these hostnames (Stalwart's /admin, /login, /jmap …)
|
||||||
|
# falls through to Traefik's 404 — the management surface stays internal.
|
||||||
|
# No HTTPS-redirect middleware on purpose: Thunderbird probes plain HTTP and
|
||||||
|
# Outlook POSTs, which doesn't survive a 301 in all clients; both schemes
|
||||||
|
# serve the same answer.
|
||||||
|
#
|
||||||
|
# DNS at simply.com: autoconfig + autodiscover CNAME → mail.dezky.eu (the
|
||||||
|
# records are listed on the portal's Domains page).
|
||||||
|
#
|
||||||
|
# Customer domains (autodiscover.<customer>.tld) need per-domain certs and an
|
||||||
|
# automated Ingress/Certificate per verified domain — follow-up feature.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: stalwart-http
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: stalwart-http
|
||||||
|
app.kubernetes.io/part-of: dezky
|
||||||
|
spec:
|
||||||
|
# No selector — Stalwart runs on the HOST, not in a pod. The Endpoints
|
||||||
|
# object below pins the cni0 gateway address pods/Traefik can reach.
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Endpoints
|
||||||
|
metadata:
|
||||||
|
name: stalwart-http
|
||||||
|
subsets:
|
||||||
|
- addresses:
|
||||||
|
- ip: 10.42.0.1
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8080
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: mail-autodiscovery
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- autodiscover.dezky.eu
|
||||||
|
- autoconfig.dezky.eu
|
||||||
|
secretName: autodiscovery-dezky-eu-tls
|
||||||
|
rules:
|
||||||
|
- host: autodiscover.dezky.eu
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
# Outlook probes both capitalizations.
|
||||||
|
- path: /autodiscover/autodiscover.xml
|
||||||
|
pathType: Exact
|
||||||
|
backend: { service: { name: stalwart-http, port: { number: 8080 } } }
|
||||||
|
- path: /Autodiscover/Autodiscover.xml
|
||||||
|
pathType: Exact
|
||||||
|
backend: { service: { name: stalwart-http, port: { number: 8080 } } }
|
||||||
|
- host: autoconfig.dezky.eu
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /mail/config-v1.1.xml
|
||||||
|
pathType: Exact
|
||||||
|
backend: { service: { name: stalwart-http, port: { number: 8080 } } }
|
||||||
@@ -43,12 +43,36 @@ export class DnsVerifierService {
|
|||||||
case 'dmarc':
|
case 'dmarc':
|
||||||
return this.checkDmarc(domain)
|
return this.checkDmarc(domain)
|
||||||
case 'autodiscovery':
|
case 'autodiscovery':
|
||||||
return this.checkSrv(record.fqdn, record.expected)
|
return record.type === 'CNAME'
|
||||||
|
? this.checkCname(record.fqdn, record.expected)
|
||||||
|
: this.checkSrv(record.fqdn, record.expected)
|
||||||
default:
|
default:
|
||||||
return { status: 'pending' }
|
return { status: 'pending' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Autodiscovery CNAME (autoconfig/autodiscover hosts). An A record pointing
|
||||||
|
// at the same place also works fine in practice, so a resolvable name that
|
||||||
|
// doesn't CNAME to the expected target is warn, not bad.
|
||||||
|
private async checkCname(fqdn: string, expected: string): Promise<CheckResult> {
|
||||||
|
const expTarget = expected.trim().replace(/\.$/, '').toLowerCase()
|
||||||
|
try {
|
||||||
|
const cnames = await this.resolver.resolveCname(fqdn)
|
||||||
|
const observed = cnames.join(', ')
|
||||||
|
const hit = cnames.some((c) => c.replace(/\.$/, '').toLowerCase() === expTarget)
|
||||||
|
return hit ? { observed, status: 'ok' } : { observed, status: 'warn' }
|
||||||
|
} catch {
|
||||||
|
// No CNAME — accept a direct A record to anything (can't cheaply prove
|
||||||
|
// it's us), warn so the page nudges toward the canonical record.
|
||||||
|
try {
|
||||||
|
const a = await this.resolver.resolve4(fqdn)
|
||||||
|
return a.length ? { observed: a.join(', '), status: 'warn' } : { status: 'bad' }
|
||||||
|
} catch {
|
||||||
|
return { status: 'bad' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Autodiscovery SRV (RFC 6186): the record must exist and point at our mail
|
// Autodiscovery SRV (RFC 6186): the record must exist and point at our mail
|
||||||
// host on the expected port. expected is the zone's RDATA, e.g.
|
// host on the expected port. expected is the zone's RDATA, e.g.
|
||||||
// "0 1 993 mail.dezky.eu." — we compare target + port and ignore
|
// "0 1 993 mail.dezky.eu." — we compare target + port and ignore
|
||||||
|
|||||||
@@ -439,6 +439,11 @@ function classify(z: StalwartZoneRecord, domain: string): RecordKind | null {
|
|||||||
// that port belongs to Traefik on the node, not Stalwart, so publishing
|
// that port belongs to Traefik on the node, not Stalwart, so publishing
|
||||||
// them would advertise endpoints that 404. Revisit with the webmail story.
|
// them would advertise endpoints that 404. Revisit with the webmail story.
|
||||||
if (z.type === 'SRV' && /^_(imaps|submissions|pop3s)\._tcp\./.test(z.fqdn)) return 'autodiscovery'
|
if (z.type === 'SRV' && /^_(imaps|submissions|pop3s)\._tcp\./.test(z.fqdn)) return 'autodiscovery'
|
||||||
|
// HTTP autodiscovery hosts (Outlook's autodiscover, Thunderbird's
|
||||||
|
// autoconfig) — routed through Traefik to Stalwart's discovery endpoints.
|
||||||
|
if (z.type === 'CNAME' && (z.fqdn === `autoconfig.${domain}` || z.fqdn === `autodiscover.${domain}`)) {
|
||||||
|
return 'autodiscovery'
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user