traefik: add Traefik v3 edge stack to replace nginx-proxy-manager

Users report desktop Outlook cannot be configured from outside the office, while
OWA and mobile keep working — the connection-bound Windows auth signature. NPM
(OpenResty) terminates HTTP, so NTLM/Negotiate legs land on different pooled
upstream connections and the handshake never completes. SNI pass-through puts TLS
termination back on Exchange and preserves the auth end-to-end.

Also removes a second, quantified defect: ~82,000 ActiveSync Cmd=Ping 504s in the
retained log window (~3,400/day, 100% of all 504s). NPM has no custom snippet on
this host, so the default proxy_read_timeout 60s truncates every push heartbeat.
Under pass-through IIS owns the long-poll and there is no proxy timeout to tune.

Layout follows the proven 19-nutrilent stack, with two deliberate differences:

- ACME uses TLS-ALPN-01, not HTTP-01. httpChallenge installs Traefik's own
  handler on :80 for /.well-known/acme-challenge/, which would compete with
  forwarding that same path to Exchange for ITS renewal. TLS-ALPN runs entirely
  on :443 and leaves :80 free; the Exchange SNIs are pass-through and never
  request a Traefik cert, so there is no overlap.
- The http->https redirect is a normal priority:1 catch-all router rather than an
  entrypoint-level redirection, which would be installed at near-max priority and
  swallow the ACME path before exchange-acme (priority:1000) could match it.

That exchange-acme router is the point: it forwards the challenge path for the 24
Exchange names to 192.168.0.6:80, so win-acme on SERVERMAIL renews itself without
the manual port-80 NAT repoint every past renewal has needed. It requires the edge
:80 NAT to point at the docker host (192.168.0.9) — it is currently on Exchange
and must be moved back as part of the cutover. Called out in the README.

Pass-through covers 24 SNIs — exactly the SAN list of Exchange's current cert.
That is the 21 names NPM published plus three that had no NPM vhost at all and
currently fail the TLS handshake outright (mail/autodiscover.alisailina.com,
autodiscover.neviol.ru); they are on the cert already, so including them fixes
them at no cost.

Not migrated: nginx.shcnw.ru (NPM's own admin UI) and nginxlogs.shcnw.ru
(goaccess parses NPM's logs, and Exchange — 99.1% of traffic — moves to L4 where
there is no HTTP access log; retire it).

Staged on the host and pre-flighted with a real Traefik container on
reverseproxy-nw with NO published ports, so it could not touch NPM's :443:
config parses, every router/service/middleware resolves, ChallengeTLSALPN
provider starts. Config checksums verified against this repo. The only errors are
ACME issuance failures, expected while NPM still holds :443.

Two things the pre-flight caught:

- traefik.shcnw.ru is NXDOMAIN, so the dashboard router is shipped commented out.
  ACME validates from the internet even for a LAN-only router. Create the A record
  and uncomment (the file hot-reloads); the htpasswd is already staged on the host.
- All other L7 names and all 24 pass-through names resolve to 217.15.22.194.

Deliberately unproven and flagged in the README: that TLS-ALPN issuance works
while a TCP pass-through router shares the entrypoint. The SNI sets are disjoint
so it should, but nothing here demonstrates it — hence the recommendation to make
the first start use the Let's Encrypt staging CA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 17:53:02 +03:00
co-authored by Claude Opus 5
parent 4420ff17dc
commit 537c2afc46
7 changed files with 506 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
# ─────────────────────────────────────────────────────────────────────────────
# Traefik STATIC configuration — 43-meditsina
# Host path: /mnt/containers/traefik/container-data/traefik.yml
# Changes here need a container restart (the dynamic/ files hot-reload instead).
# ─────────────────────────────────────────────────────────────────────────────
global:
checkNewVersion: false
sendAnonymousUsage: false
entryPoints:
web:
address: ":80"
# NOTE: NO entrypoint-level http->https redirection here, deliberately.
# An entrypoint redirect is installed as an internal router at near-max priority,
# which would swallow the /.well-known/acme-challenge/ path before the
# `exchange-acme` router could forward it to Exchange. The redirect is instead a
# normal low-priority catch-all router in dynamic/services.yml, so the ACME
# router can outrank it deterministically.
websecure:
address: ":443"
# NO default TLS cert/resolver on the entrypoint — the Exchange TCP pass-through
# router must own the raw TLS for its SNIs. L7 hosts set certResolver per-router.
providers:
file:
directory: /etc/traefik/dynamic
watch: true
certificatesResolvers:
le:
acme:
email: admin@shcnw.ru # ← set a MONITORED mailbox before deploying
storage: /etc/traefik/acme/acme.json
# TLS-ALPN-01, not HTTP-01, and this is load-bearing:
# httpChallenge would install Traefik's own handler on :80 for
# /.well-known/acme-challenge/, which competes with forwarding that same path to
# Exchange for ITS renewal. TLS-ALPN runs entirely on :443 and leaves :80 free.
# The Exchange SNIs are pass-through and never request a Traefik cert, so the
# ALPN challenge only ever runs for the L7 names below — no overlap.
tlsChallenge: {}
# ── FIRST CUTOVER: uncomment to use Let's Encrypt STAGING and avoid burning
# rate limits while shaking out DNS/ports. Then re-comment, truncate
# acme.json, and restart so production certs issue. ──
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory
api:
dashboard: true # reachable only via the LAN-only + basic-auth router
log:
level: INFO
accessLog:
filePath: /var/log/traefik/access.log
format: common
bufferingSize: 100