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>
187 lines
8.5 KiB
YAML
187 lines
8.5 KiB
YAML
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Traefik DYNAMIC configuration (file provider) — 43-meditsina
|
|
# Host path: /mnt/containers/traefik/container-data/dynamic/services.yml
|
|
# Hot-reloaded on save (watch: true) — no container restart needed for edits here.
|
|
#
|
|
# Replaces 13 nginx-proxy-manager proxy hosts. Two are deliberately NOT migrated:
|
|
# nginx.shcnw.ru — NPM's own admin UI; NPM is gone
|
|
# nginxlogs.shcnw.ru — goaccess; it parses NPM's logs, and Exchange (99% of traffic)
|
|
# moves to L4 where there is no HTTP access log. Retire it.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
tcp:
|
|
# ===========================================================================
|
|
# Exchange — SNI PASS-THROUGH (Layer 4).
|
|
# TLS terminates ON Exchange (192.168.0.6), so connection-bound Windows auth
|
|
# (NTLM / Negotiate) survives end-to-end → desktop Outlook can be configured
|
|
# from outside. Also removes the ActiveSync `Cmd=Ping` 504s: IIS owns the
|
|
# long-poll, so nginx's 60s proxy_read_timeout no longer truncates it.
|
|
#
|
|
# Exchange presents its own 24-SAN public LE cert (C178CFB8…, exp 2026-11-24);
|
|
# Traefik must NOT terminate or manage a cert for these names.
|
|
#
|
|
# The 24 SNIs below are exactly that certificate's SAN list. Three of them
|
|
# (mail/autodiscover.alisailina.com, autodiscover.neviol.ru) have no NPM vhost
|
|
# today and currently fail the TLS handshake outright — including them here
|
|
# fixes them at no cost. See the migration plan §5.
|
|
# ===========================================================================
|
|
routers:
|
|
exchange:
|
|
entryPoints: [websecure]
|
|
# v3 REJECTS HostSNI(`a`,`b`) — they must be OR-ed.
|
|
rule: >-
|
|
HostSNI(`mail.shcnw.ru`) || HostSNI(`autodiscover.shcnw.ru`)
|
|
|| HostSNI(`mail.exprinta.ru`) || HostSNI(`autodiscover.exprinta.ru`)
|
|
|| HostSNI(`mail.inkam.navy`) || HostSNI(`autodiscover.inkam.navy`)
|
|
|| HostSNI(`mail.inkam.parts`) || HostSNI(`autodiscover.inkam.parts`)
|
|
|| HostSNI(`mail.nt.parts`) || HostSNI(`autodiscover.nt.parts`)
|
|
|| HostSNI(`mail.ankil.ltd`) || HostSNI(`autodiscover.ankil.ltd`)
|
|
|| HostSNI(`mail.pda.ae`) || HostSNI(`autodiscover.pda.ae`)
|
|
|| HostSNI(`mail.nordtextile.ru`) || HostSNI(`autodiscover.nordtextile.ru`)
|
|
|| HostSNI(`mail.setto.ru`) || HostSNI(`autodiscover.setto.ru`)
|
|
|| HostSNI(`mail.smartdtl.ru`) || HostSNI(`autodiscover.smartdtl.ru`)
|
|
|| HostSNI(`mail.neviol.ru`) || HostSNI(`autodiscover.neviol.ru`)
|
|
|| HostSNI(`mail.alisailina.com`) || HostSNI(`autodiscover.alisailina.com`)
|
|
tls:
|
|
passthrough: true
|
|
service: exchange
|
|
services:
|
|
exchange:
|
|
loadBalancer:
|
|
servers:
|
|
- address: "192.168.0.6:443"
|
|
|
|
http:
|
|
# ===========================================================================
|
|
# Middlewares
|
|
# ===========================================================================
|
|
middlewares:
|
|
lan-only: # = NPM "Access List: allow 192.168.0.0/22; deny all"
|
|
ipAllowList:
|
|
sourceRange:
|
|
- "192.168.0.0/22"
|
|
dash-auth:
|
|
basicAuth:
|
|
usersFile: /etc/traefik/users/dashboard.htpasswd
|
|
redirect-https:
|
|
redirectScheme:
|
|
scheme: https
|
|
permanent: true
|
|
|
|
# Backends presenting self-signed certs (NPM forwarded with "verify SSL" off).
|
|
serversTransports:
|
|
insecure:
|
|
insecureSkipVerify: true
|
|
|
|
# ===========================================================================
|
|
# Routers on :80 (web)
|
|
# ===========================================================================
|
|
routers:
|
|
# --- Forward the ACME challenge for the Exchange names to Exchange itself. ---
|
|
# This is what makes win-acme's HTTP-01 SelfHosting validation on SERVERMAIL work
|
|
# WITHOUT the manual port-80 NAT repoint that every past renewal has needed.
|
|
# REQUIRES the edge NAT for :80 to point at this docker host (192.168.0.9).
|
|
# Priority must beat the catch-all redirect below.
|
|
exchange-acme:
|
|
entryPoints: [web]
|
|
priority: 1000
|
|
rule: >-
|
|
PathPrefix(`/.well-known/acme-challenge/`) && (
|
|
Host(`mail.shcnw.ru`) || Host(`autodiscover.shcnw.ru`)
|
|
|| Host(`mail.exprinta.ru`) || Host(`autodiscover.exprinta.ru`)
|
|
|| Host(`mail.inkam.navy`) || Host(`autodiscover.inkam.navy`)
|
|
|| Host(`mail.inkam.parts`) || Host(`autodiscover.inkam.parts`)
|
|
|| Host(`mail.nt.parts`) || Host(`autodiscover.nt.parts`)
|
|
|| Host(`mail.ankil.ltd`) || Host(`autodiscover.ankil.ltd`)
|
|
|| Host(`mail.pda.ae`) || Host(`autodiscover.pda.ae`)
|
|
|| Host(`mail.nordtextile.ru`) || Host(`autodiscover.nordtextile.ru`)
|
|
|| Host(`mail.setto.ru`) || Host(`autodiscover.setto.ru`)
|
|
|| Host(`mail.smartdtl.ru`) || Host(`autodiscover.smartdtl.ru`)
|
|
|| Host(`mail.neviol.ru`) || Host(`autodiscover.neviol.ru`)
|
|
|| Host(`mail.alisailina.com`) || Host(`autodiscover.alisailina.com`)
|
|
)
|
|
service: exchange-http
|
|
|
|
# --- Everything else on :80 redirects to https (replaces the entrypoint-level
|
|
# redirect, which would outrank exchange-acme). Lowest priority. ---
|
|
http-catchall:
|
|
entryPoints: [web]
|
|
priority: 1
|
|
rule: "PathPrefix(`/`)"
|
|
middlewares: [redirect-https]
|
|
service: noop
|
|
|
|
# =========================================================================
|
|
# Routers on :443 (websecure) — L7, Traefik terminates and manages the cert
|
|
# =========================================================================
|
|
crm:
|
|
entryPoints: [websecure]
|
|
rule: "Host(`crm.shcnw.ru`) || Host(`crm.inkam.navy`)"
|
|
service: crm
|
|
tls: { certResolver: le }
|
|
onec:
|
|
entryPoints: [websecure]
|
|
rule: "Host(`1c.exprinta.ru`)"
|
|
service: onec
|
|
tls: { certResolver: le }
|
|
|
|
# ---- LAN-only (internal admin) ----
|
|
start:
|
|
entryPoints: [websecure]
|
|
rule: "Host(`start.shcnw.ru`)"
|
|
service: flame
|
|
middlewares: [lan-only]
|
|
tls: { certResolver: le }
|
|
portainer:
|
|
entryPoints: [websecure]
|
|
rule: "Host(`portainer.shcnw.ru`)"
|
|
service: portainer
|
|
middlewares: [lan-only]
|
|
tls: { certResolver: le }
|
|
cockpit:
|
|
entryPoints: [websecure]
|
|
rule: "Host(`cockpit.shcnw.ru`)"
|
|
service: cockpit
|
|
middlewares: [lan-only]
|
|
tls: { certResolver: le }
|
|
# ---- Traefik dashboard: DISABLED until DNS exists ----
|
|
# `traefik.shcnw.ru` is NXDOMAIN as of 2026-08-26. ACME (TLS-ALPN, like HTTP-01)
|
|
# validates from the internet, so a public A-record -> 217.15.22.194 is required
|
|
# even though the router is LAN-only. Create the record, then uncomment — this
|
|
# file hot-reloads, no restart needed. The htpasswd is already staged on the host.
|
|
# dashboard:
|
|
# entryPoints: [websecure]
|
|
# rule: "Host(`traefik.shcnw.ru`)"
|
|
# service: api@internal
|
|
# middlewares: [lan-only, dash-auth]
|
|
# tls: { certResolver: le }
|
|
|
|
# ===========================================================================
|
|
# Services (backends) — mirror the NPM proxy-host forward targets
|
|
# ===========================================================================
|
|
services:
|
|
exchange-http: # :80 ACME challenge forward only (see exchange-acme)
|
|
loadBalancer:
|
|
servers: [{ url: "http://192.168.0.6:80" }]
|
|
crm: # Bitrix — self-signed cert (CN=Bitrix, 2023→2033)
|
|
loadBalancer:
|
|
serversTransport: insecure
|
|
servers: [{ url: "https://192.168.0.10:443" }]
|
|
onec: # Apache/2.4.23 (Win64), plain HTTP
|
|
loadBalancer:
|
|
servers: [{ url: "http://192.168.0.4:80" }]
|
|
flame: # sibling container on reverseproxy-nw
|
|
loadBalancer:
|
|
servers: [{ url: "http://flame:5005" }]
|
|
portainer:
|
|
loadBalancer:
|
|
serversTransport: insecure
|
|
servers: [{ url: "https://192.168.0.9:9443" }]
|
|
cockpit:
|
|
loadBalancer:
|
|
serversTransport: insecure
|
|
servers: [{ url: "https://192.168.0.9:9090" }]
|
|
noop: # never reached — http-catchall always redirects first
|
|
loadBalancer:
|
|
servers: [{ url: "http://127.0.0.1:1" }]
|