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
+3
View File
@@ -0,0 +1,3 @@
# Portainer stack environment for the traefik stack.
# Set this in the Portainer stack's "Environment variables" panel.
TZ=Europe/Moscow
+6
View File
@@ -0,0 +1,6 @@
# Secrets and runtime state — these live ONLY on the host, never in git.
acme/
users/*.htpasswd
!users/*.htpasswd.example
logs/
.env
+211
View File
@@ -0,0 +1,211 @@
# traefik — edge reverse proxy for 43-meditsina
Single **Traefik v3** instance replacing **nginx-proxy-manager** on the docker host
(`192.168.0.9`). It does three jobs on the one public `:443` / `:80`:
| Job | Hosts | How |
|-----|-------|-----|
| **SNI pass-through (L4)** | 24 `mail.*` / `autodiscover.*` names → Exchange `192.168.0.6:443` | TLS terminates **on Exchange** → native NTLM/Negotiate survives → **desktop Outlook can be set up from outside the office** |
| **TLS-terminate + Let's Encrypt (L7)** | `crm.*`, `1c.exprinta.ru`, `start`, `portainer`, `cockpit` | Traefik terminates, auto-issues/renews, and preserves the real client IP so `ipAllowList` still works |
| **ACME forward (L7 on `:80`)** | the same 24 Exchange names, `/.well-known/acme-challenge/` only | forwarded to `192.168.0.6:80` so **win-acme on SERVERMAIL renews itself without the manual NAT repoint** |
**Why this migration:** an HTTP-terminating proxy (NPM = OpenResty) breaks connection-bound Windows
auth — NTLM/Negotiate must complete on one TCP connection, and NPM spreads the legs across pooled
upstream connections. Symptom reported by users: *Outlook cannot be configured from outside the
office*, while OWA and mobile (stateless auth) keep working. Pass-through fixes it end-to-end.
It 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's default `proxy_read_timeout 60s` truncates every
ActiveSync push heartbeat; under pass-through IIS owns the long-poll and there is no proxy timeout.
Full rationale and evidence:
`nextcloud/diagnostics/2026-08-26-43-meditsina-docker-npm-to-traefik-migration-plan.md`.
Modelled on the proven 19-nutrilent and 05-osk deployments.
---
## Files
| Repo file | Purpose | Goes on host at |
|-----------|---------|-----------------|
| `docker-compose.yaml` | the stack — **the only file Portainer pulls** | pulled by Portainer GitOps |
| `traefik.yml` | **static** config (entrypoints, ACME, providers) | `/mnt/containers/traefik/container-data/traefik.yml` |
| `dynamic/services.yml` | **dynamic** config (routers/services/middlewares) — hot-reloads | `/mnt/containers/traefik/container-data/dynamic/services.yml` |
| `users/dashboard.htpasswd.example` | template only | real file created on host, gitignored |
> **Portainer pulls only `docker-compose.yaml`.** The config files must already be on the host at
> **absolute** paths — Portainer runs compose relative to its *own* container, so relative `./mounts`
> resolve to empty dirs and Traefik crash-loops. Keep host copies in sync with this repo on every
> change.
---
## Two design decisions that differ from the 19-nutrilent stack
**1. ACME uses TLS-ALPN-01, not HTTP-01.** `httpChallenge` makes Traefik install its 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.
**2. The http→https redirect is a normal router, not an entrypoint redirection.** An
entrypoint-level redirect is installed as an internal router at near-maximum priority and would
swallow the ACME path before `exchange-acme` could match. It is therefore a `priority: 1` catch-all
router in `dynamic/services.yml`, with `exchange-acme` at `priority: 1000`.
---
## Host preparation — **already done 2026-08-26**
Recorded here for rebuilds. Staged and pre-flighted on `docker` (43); config checksums verified
against this repo.
```bash
ssh -p 43009 -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes root@localhost
H=/mnt/containers/traefik/container-data
mkdir -p $H/dynamic $H/users $H/acme $H/logs
# copy traefik.yml and dynamic/services.yml from a checkout — write to a temp name and
# `mv` into place (atomic): a plain scp truncates-then-writes and the file watcher can
# read mid-write, throwing a transient `yaml: mapping key already defined`.
touch $H/acme/acme.json && chmod 600 $H/acme/acme.json # 600 or Traefik refuses to start
# dashboard basic-auth — htpasswd is NOT installed on this host, use openssl:
PW="$(openssl rand -base64 15)"
printf 'admin:%s\n' "$(openssl passwd -apr1 "$PW")" > $H/users/dashboard.htpasswd
chmod 600 $H/users/dashboard.htpasswd; echo "$PW" # store in the password manager
```
`reverseproxy-nw` already exists. SELinux is **Enforcing** — every mount carries `:Z`.
### Pre-flight (safe: no published ports, cannot touch NPM's `:443`)
```bash
timeout 15 docker run --rm --name traefik-precheck --network reverseproxy-nw \
-v $H/traefik.yml:/etc/traefik/traefik.yml:ro,Z -v $H/dynamic:/etc/traefik/dynamic:ro,Z \
-v $H/users:/etc/traefik/users:ro,Z -v $H/acme:/etc/traefik/acme:Z \
-v $H/logs:/var/log/traefik:Z traefik:v3.3
: > $H/acme/acme.json && chmod 600 $H/acme/acme.json # precheck may register an ACME account
```
Result 2026-08-26: config parses, all routers/services/middlewares resolve, `ChallengeTLSALPN`
provider starts. The only errors are ACME issuance failures — **expected**, because NPM still holds
`:443` so the ALPN challenge cannot be answered yet.
---
## ⚠️ Before cutover — two prerequisites
**1. Port 80 must be NAT'd to the docker host (`192.168.0.9`).** It is currently pointed at
**Exchange** (`192.168.0.6`) — repointed by hand on 2026-08-26 so win-acme could renew. Traefik's
`exchange-acme` router is what replaces that manual step permanently, but it can only do so if `:80`
arrives at Traefik. **Move it back to `192.168.0.9` as part of the cutover.**
**2. `traefik.shcnw.ru` has no DNS record.** The dashboard router is shipped **commented out** for
that reason — ACME validates from the internet even for a LAN-only router. Create
`traefik.shcnw.ru A 217.15.22.194`, then uncomment the router (hot-reloads, no restart). The
htpasswd is already staged.
---
## Deploy (Portainer GitOps)
1. Push this `traefik/` folder.
2. Portainer → **Stacks → Add stack → Repository** → this repo, branch `main`, compose path
`traefik/docker-compose.yaml`, env `TZ=Europe/Moscow`. **Do not start it while NPM holds `:443`**
Traefik will crash-loop on the port conflict.
### Recommended: first start against Let's Encrypt STAGING
The one thing the pre-flight could **not** prove is that TLS-ALPN issuance works while a TCP
pass-through router shares the same entrypoint (the SNI sets are disjoint, so it should — but it is
unproven here). Validate it without burning production rate limits:
```bash
# in traefik.yml, uncomment:
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory
# cut over, confirm all 5 L7 routers get a (staging) cert and Exchange pass-through works, then:
# re-comment caServer, truncate acme.json, restart the container -> production certs issue
```
## Cutover
```bash
docker stop nginx-proxy-manager nginx-proxy-manager-db goaccess # keep DEFINED for rollback
# deploy/start the traefik stack in Portainer
```
Then repoint the edge `:80` NAT to `192.168.0.9`.
**Rollback is instant** — NPM's data and config are untouched:
```bash
docker stop traefik && docker start nginx-proxy-manager nginx-proxy-manager-db
```
Keep NPM stopped-but-defined for ~1 week, then decommission.
---
## Verification
```bash
# 1. Pass-through is real L4 — the SNI serves EXCHANGE's own cert, not a Traefik cert
echo | openssl s_client -connect 127.0.0.1:443 -servername mail.pda.ae 2>/dev/null \
| openssl x509 -noout -issuer -subject -ext subjectAltName
# -> issuer Let's Encrypt, subject CN=mail.alisailina.com, 24 SANs (NOT a Traefik-issued cert)
# 2. An L7 host gets a Traefik-issued cert
echo | openssl s_client -connect 127.0.0.1:443 -servername 1c.exprinta.ru 2>/dev/null \
| openssl x509 -noout -issuer -subject
# 3. ipAllowList works BOTH ways (proves Traefik sees the real client IP)
curl -sk -o /dev/null -w '%{http_code}\n' --resolve start.shcnw.ru:443:127.0.0.1 https://start.shcnw.ru/ # 403
curl -sk -o /dev/null -w '%{http_code}\n' --resolve start.shcnw.ru:443:192.168.0.9 https://start.shcnw.ru/ # 200/302
# 4. Self-signed backends must not 502 (proves serversTransport is wired)
curl -sk -o /dev/null -w 'crm=%{http_code}\n' --resolve crm.shcnw.ru:443:192.168.0.9 https://crm.shcnw.ru/
# 5. The ACME forward reaches Exchange rather than being redirected to https
curl -sI http://mail.shcnw.ru/.well-known/acme-challenge/probe # -> from IIS, NOT a 301
curl -sI http://mail.shcnw.ru/ # -> 301 to https (catch-all)
# 6. Container stable, no real errors
docker inspect traefik --format 'RestartCount={{.RestartCount}} Running={{.State.Running}}'
docker logs traefik --since 5m 2>&1 | grep -iE "level=err" | grep -vi "connection reset by peer"
```
**End-to-end — the actual test:** configure a **desktop Outlook profile off-network**. It must
autodiscover and authenticate. Then confirm OWA and mobile ActiveSync still work, that ActiveSync
`Ping` now holds well past 60 s, and that an external browser is refused on the LAN-only hosts.
> **Benign noise:** `Error while handling TCP connection … 192.168.0.6:443 … connection reset by peer`
> on the pass-through router is normal L4 keep-alive churn, **not** a fault — provided there are
> **zero** TLS/SNI/handshake errors and `RestartCount` stays 0.
---
## Backend map (what replaced what)
| Old NPM proxy host | Domain(s) | Backend | Path here |
|---|---|---|---|
| 1, 9, 11, 12, 13, 14 | 21 × `mail.*` / `autodiscover.*` | `192.168.0.6:443` | **TCP passthrough** `exchange` |
| — | +3 names that had **no** NPM vhost and failed the handshake outright (`mail`/`autodiscover.alisailina.com`, `autodiscover.neviol.ru`) | `192.168.0.6:443` | same router — **fixed for free**, they are on Exchange's cert |
| 2 | `crm.shcnw.ru`, `crm.inkam.navy` | `192.168.0.10:443` (Bitrix, self-signed) | `crm` (+`insecure` transport) |
| 10 | `1c.exprinta.ru` | `192.168.0.4:80` (Apache 2.4.23 Win64) | `onec` |
| 3 | `start.shcnw.ru` | `flame:5005` | `start` (LAN-only) |
| 5 | `portainer.shcnw.ru` | `192.168.0.9:9443` | `portainer` (LAN-only) |
| 6 | `cockpit.shcnw.ru` | `192.168.0.9:9090` | `cockpit` (LAN-only) |
| 4 | `nginx.shcnw.ru` (NPM admin UI) | — | **dropped** — NPM is gone |
| 8 | `nginxlogs.shcnw.ru` (goaccess) | — | **dropped** — see below |
## Post-cutover follow-ups
- **Retire goaccess.** It parses NPM's logs, which go stale once NPM stops, and Exchange — 99.1% of
traffic — moves to L4 where there is no HTTP access log. Remove the `goacess-for-nginx` stack and
drop the `nginxlogs.shcnw.ru` DNS record. Use Traefik's native Prometheus metrics instead.
- **Drop the `nginx.shcnw.ru` DNS record.**
- **NPM's admin UI was published on `0.0.0.0:81`**, bypassing its own `192.168.0.0/22` ACL. That
exposure disappears with NPM; confirm nothing at the edge still forwards `:81`.
- **Decide `rdp.inkam.navy`** — it resolves here and has a stale NPM cert but no vhost; publish or retire.
- **Exchange keeps renewing its own certificate.** With `exchange-acme` in place that becomes
automatic; verify at the next renewal (**due 2026-10-20**, cert expires 2026-11-24) that win-acme
succeeds *without* a NAT change.
+37
View File
@@ -0,0 +1,37 @@
# Traefik v3 — single edge reverse proxy for 43-meditsina (replaces nginx-proxy-manager).
#
# • SNI pass-through (L4) for Exchange → native NTLM/Kerberos survives → desktop Outlook
# can be set up from outside the office (the reason for this migration)
# • TLS-terminate + Let's Encrypt autorenew (L7) for the remaining web services
# • Forwards the ACME challenge path for the Exchange names to Exchange itself, so its
# win-acme renewal stops needing a manual port-80 NAT repoint (see README)
#
# Deployed via Portainer GitOps. IMPORTANT: Portainer pulls ONLY this docker-compose.yaml.
# The config files must be staged on the HOST at absolute paths beforehand — Portainer runs
# compose relative to its own container, so relative ./mounts resolve to empty dirs and
# Traefik crash-loops. See README.md → "One-time host preparation".
#
# Host: docker (43) — 192.168.0.9, tunnel port 43009. SELinux is Enforcing, hence `:Z`.
services:
traefik:
image: traefik:v3.3
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- TZ=${TZ:-Europe/Moscow}
volumes:
- /mnt/containers/traefik/container-data/traefik.yml:/etc/traefik/traefik.yml:ro,Z
- /mnt/containers/traefik/container-data/dynamic:/etc/traefik/dynamic:ro,Z
- /mnt/containers/traefik/container-data/users:/etc/traefik/users:ro,Z
- /mnt/containers/traefik/container-data/acme:/etc/traefik/acme:Z
- /mnt/containers/traefik/container-data/logs:/var/log/traefik:Z
networks:
- reverseproxy-nw
networks:
reverseproxy-nw:
external: true
+186
View File
@@ -0,0 +1,186 @@
# ─────────────────────────────────────────────────────────────────────────────
# 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" }]
+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
+7
View File
@@ -0,0 +1,7 @@
# Real file lives ONLY on the host at
# /mnt/containers/traefik/container-data/users/dashboard.htpasswd
# and is gitignored. Generate with either:
# htpasswd -nbB admin 'PASSWORD' # if httpd-tools is installed
# printf 'admin:%s\n' "$(openssl passwd -apr1 'PASSWORD')" # no extra packages needed
# then: chmod 600 dashboard.htpasswd
admin:$apr1$EXAMPLE$EXAMPLEEXAMPLEEXAMPLE0