Port :8089/:4443 утечка в admin URLs закрыта. URL Rewrite 2.1 + apphost allowedServerVariables (HTTPS/SERVER_PORT/SERVER_PORT_SECURE) + Web.config rule на X-Forwarded-Proto=https → set SERVER_PORT=443/SERVER_PORT_SECURE=1/HTTPS=on ДО того как ASP.NET читает их в Url.SiteRoot(). Лечит все 11 cms (общий site snolla). Path B (traefik http entrypoint :80→:8090) abandoned — Docker Desktop WSL2 NAT quirk на host.docker.internal:80 возвращает 17-byte 301 plain text независимо от traefik internals. Quirk не reproducible на Linux Docker (synology). Side regression: pre-existing customErrors mode="off" lowercase в Web.config пробудился после ASP.NET full-reload (мой rewrite-block edit) — fixed (Off). Outside scope, open: emspb.snolla.com /admin/assets/<guid>/getList → 500 NullRef в AssetsJsonViewModelBuilder.cs:22 (model null от GetFolderByPath). User подтвердил «только этот site». Зафиксировано в snapshot open issue #8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
cms-port-leak-fix
Goal
Убрать утечку internal-портов в URL'ы, которые CMS генерирует серверно. Сейчас часть admin-роутов выдают URL'ы вида https://labtools.snolla.com:8089/admin/... — :8089 это IIS site binding на хосте, не должен попадать наружу.
Связанный, тот же класс баг (open issue #1 в recovery-architecture-snapshot.md): CMS делает HTTP→HTTPS redirect с :4443 (traefik external HTTPS port на хосте). Желательно фиксить общим решением.
Симптомы (от user, 2026-05-19)
Сломанные URLs admin'ки на labtools.snolla.com (наверняка и на остальных 10 cms доменах тоже):
https://labtools.snolla.com:8089/admin/assets/<guid>/getList?path=https://labtools.snolla.com:8089/admin/themes/getImageSizes/https://labtools.snolla.com:8089/admin/templates/editors.tmpl.html?v=2.006
Browser сразу делает request на :8089 напрямую → fail (этот порт не открыт наружу через router NAT; traefik слушает 443/4443 only).
Hypothesis — root cause
Цепочка:
- Traefik backend =
http://host.docker.internal:8089/,passHostHeader: true→ IIS видитHost: labtools.snolla.com(без порта). - IIS binding на site
snolla=*:8089→ connection-level порт = 8089. HttpRequest.Url.Port/Url.Authorityв ASP.NET возвращают порт сокета, не порт из Host header. ЗначитAuthority = "labtools.snolla.com:8089".- Где-то в admin views / controllers / partials CMS строит absolute URL через
Request.Url.GetLeftPart(UriPartial.Authority)илиstring.Format("{0}://{1}", scheme, host_with_port)→ утечка:8089. - Альтернатива — admin SPA получает baseUrl в server-rendered JS-переменной (
window.adminBaseUrl = '@Request.Url.Scheme://@Request.Url.Authority/admin'или подобное). Это нужно подтвердить grep'ом по deployed-ASP-views + .js.
Аналогичная hypothesis для :4443:
- Не из
Url.Port(он = 8089 на attempt2-host, был 18080 на VM-эпохе). - Скорее CMS читает
Request.Headers["X-Forwarded-Host"]который traefik по умолчанию шлёт какhost:port, ИЛИ кто-то один раз протестил черезhttps://host:4443/напрямую и попал в captured-redirect. - Возможно: CMS делает
Response.Redirectс absolute URL, и старый CMS-код в эпоху production-traefik (на синке тоже :4443?) научился возвращать публичный port из конфига. - Эта часть требует тест-кейса — повторить на host'е сейчас и посмотреть, попадает ли вообще
:4443в response Location header.
Key files (текущие deployed)
C:\sites\snolla\Views\Shared\_Layout.cshtml— main layout, естьvar hostingUrl = new Uri("https://" + AppSettings["primaryDomain"]);(line 11-12). Не порт, но паттерн «строим absolute URL» подсветить.C:\sites\snolla\Areas\Admin\Views\_ViewStart.cshtml— корень всех admin views, скорее всего тянет admin-specific layout.C:\sites\snolla\Areas\Admin\Views\**\Shared\*— admin layout (искать; возможно тут baseUrl).C:\sites\snolla\admin\templates\*.tmpl.html— статические template'ы admin SPA. Сами URL'ы не генерируют, но загружаются из JS которая видит baseUrl откуда-то.C:\sites\snolla\Web.config— appSettings (primaryDomainуже есть), будущие<rewrite>rules.C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\*.yml— site routes (тут middleware подключаем).C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\snolla.yml:1— already haspassHostHeader: true. Добавить middleware на forwarded-headers.
План этапов
Phase 1 — diagnostic (не write-op)
- Сделать XHR call на
https://labtools.snolla.com/admin/themes/getImageSizes/(с auth-cookie если есть) и записать response headers + body. Подтвердить откуда именно:8089приходит (Location header?<base>tag?<script var>? JSONbaseUrlfield?). - Найти server-side место — grep по
:8089,Request.Url.Authority,Request.Url.GetLeftPart,Url.Action(.., protocol:),UriBuilderвMoreThenCms.Web\Admin\source + deployed. - Проверить, отдаёт ли traefik сейчас
X-Forwarded-*headers backend'у (по default traefik их шлёт, ноX-Forwarded-Port= 443 entrypoint port, иX-Forwarded-Host= client Host). - Воспроизвести
:4443leak: тест с публичной сети, посмотреть Location header в HTTP→HTTPS redirect.
Phase 2 — fix через IIS URL Rewrite + ARR (RECOMMENDED)
Почему этот путь: не трогаем CMS source code, фикс на инфра-уровне, переносим только конфиг — не риск перекомпиляции. Standard ASP.NET reverse-proxy паттерн.
Шаги:
- Установить IIS modules:
URL Rewrite 2.1+Application Request Routing 3.0через MSI (download links в notes). - В
C:\sites\snolla\Web.config(и stostayer/stostayer.old аналогично) добавить inbound rewrite rule со<serverVariables>блоком — переписатьHTTPS=on,SERVER_PORT=443,SERVER_PORT_SECURE=1еслиHTTP_X_FORWARDED_PROTO=https. - В
applicationHost.config(требует admin-shell) добавить<allowedServerVariables>для всех затрагиваемых vars (HTTPS, SERVER_PORT, SERVER_PORT_SECURE, HTTP_HOST если будем переписывать). - Перезапустить IIS site / app pool, проверить
Request.Url.Port == 443через простой@Request.Url.Portecho на test-странице. - Если CMS читает
Request.Url.Hostчерез Authority — переписать такжеHTTP_HOST(но это рискованнее, может сломать вирт.хосты на IIS).
Phase 3 — traefik middleware (если Phase 2 не закрывает всё)
- Добавить
data/custom/forwarded-headers-middleware.yml:http: middlewares: forward-https: headers: customRequestHeaders: X-Forwarded-Proto: https X-Forwarded-Port: "443" - В каждый site-yml router добавить
middlewares: [forward-https]. docker restart traefik, проверить headers пробрасываются.
Phase 4 — verify
- Phone-test admin login + admin/themes/getImageSizes/ XHR — URL без портов.
- Smoke test publicly через
Invoke-WebRequestна all 11 cms domains, посмотреть Location headers на любых redirect'ах —:4443/:8089не должно нигде встречаться. - Update recovery-architecture-snapshot.md — issue #1 → resolved.
Open questions
- Можно ли вообще менять
applicationHost.configбез брейка стека? (это глобальный IIS config, не per-site). - CMS-код в admin строит absolute URL'ы — server-side (Razor) или client-side (JS)? Это меняет место fix'а.
- Bind на
*:80для IIS sitesnollaещё держится? Если да — traefik можно (теоретически) переключить на :80 backend, но это вернёт Docker NAT loopback bug attempt 1. НЕ ДЕЛАТЬ без рекомендации loop-detect recipe. *.tmpl.htmlфайлы — статичные. Кто их load'ит и откуда берёт URL? Возможно admin.js, который читает<base href>или meta tag.
Decisions log
- 2026-05-19: вынес из
iis-on-host-migration(Phase 5 «бонусы») в отдельную задачу — user попросил. Аргументация: scope крупнее чем «бонус» — затрагивает все 11 sites + traefik + потенциально applicationHost.config. Тестировать надо отдельно от migration soak. - 2026-05-19: Phase 1 diagnostic —
Url.SiteRoot()вUrlHelpers.cs:14-37подтверждён как root cause (читаетRequest.ServerVariables["SERVER_PORT"]который = 8089 на attempt2-host). VM SSH probe подтвердил bindingMoreThenCms.Web *:80:→ SERVER_PORT=80 → port stripped whitelist'ом → почему VM работала. - 2026-05-19: Path B abandoned (traefik http entrypoint :80 → :8090 + IIS backend :80) — Docker Desktop's WSL2 NAT quirk возвращает 17-byte 301 plain text на
host.docker.internal:80независимо от того что внутри traefik :80 entrypoint удалён. Quirk reproducible черезgateway.docker.internal:80и LAN IP192.168.1.143:80тоже. Traefik configs восстановлены из.bak-pre-portleak-2026-05-19, canary yml удалён. - 2026-05-19: Path C applied — URL Rewrite 2.1 + apphost allowedServerVariables + Web.config rewrite rule. Verified end-to-end через
docker exec traefik wget https://127.0.0.1:443/...(full chain включая traefik X-Forwarded-Proto auto-injection). - 2026-05-19: emspb /admin/assets 500 — user подтвердил «только этот site, остальное работает», вынесено из scope этой task'и в open issue #8
recovery-architecture-snapshot. Не блокирует closing.
Completed steps
- Phase 1 diagnostic: grep
Url\.SiteRoot\|GetLeftPart\|Url\.(Authority\|Port), найденUrlHelpers.cs:14-37. VM SSHappcmd list site /xmlподтвердил VM binding:80.findstrна VM applicationHost.config = no rewrite rules,appcmd list module= no URL Rewrite/ARR — VM working state без специальной IIS-magic. - Path B aborted: backup 3 files (traefik.yml, docker-compose.yml, emspb.yml), edit entrypoint :80→:8090, port mapping 8000:8090, canary yml для
emspb.snolla.comс backend:80.docker compose up -d --force-recreate→ recreate'ed. Loop не ушёл на host.docker.internal:80 (probe 17-byte 301). Revert configs из bak, canary yml удалён, recreate again. - Path C applied:
- URL Rewrite 2.1 MSI install (rewrite_amd64_en-US.msi, 6 MB, version 7.1.1993.2351) via elevated
Start-Process msiexec. applicationHost.configbackup.bak-pre-portleak-2026-05-19, добавлены HTTPS/SERVER_PORT/SERVER_PORT_SECURE в<rewrite>/<allowedServerVariables>через elevatedappcmd set config /+...(после неудачной попытки$cfg.OuterXmlsave которая flatt'нула 937 lines в 1 — restored from backup).C:\sites\snolla\Web.configbackup.bak-pre-portleak-2026-05-19(19050 bytes), добавлен<rewrite>/<rules>/<rule name="ForwardedProto-HTTPS">с conditionsHTTP_X_FORWARDED_PROTO=https→ setVars HTTPS/SERVER_PORT/SERVER_PORT_SECURE. UTF-8 BOM сохранён (EF BB BF verified post-edit).
- URL Rewrite 2.1 MSI install (rewrite_amd64_en-US.msi, 6 MB, version 7.1.1993.2351) via elevated
- Regression fix:
customErrors mode="off"→mode="Off"после YSOD config error (pre-existing bug пробудился ASP.NET full-reload после нашего rewrite-block edit). - Verify via docker exec traefik через full chain —
mis.siteRoot = 'https://emspb.snolla.com'иmis.siteRoot = 'https://labtools.snolla.com'(without:8089, https scheme). User-side browser test confirmed остальные 10 cms работают. - Wiki ingest: new concept
cms-server-port-leak-fix.md;traefik-on-windows-docker-desktop.mdPitfall 5 → RESOLVED;recovery-architecture-snapshot.mdopen issue #1 → resolved + новый #8 emspb assets;index.md+log.mdобновлены.
Notes
- URL Rewrite 2.1 download:
https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi - ARR 3.0 download:
https://download.microsoft.com/download/E/9/8/E9849D6A-020E-47E4-9FD0-A023E99B54EB/requestRouter_amd64.msi(не понадобился — наш fix без ARR) - Связанные wiki: cms-server-port-leak-fix (детально), traefik-on-windows-docker-desktop Pitfall 5, recovery-architecture-snapshot § Известные открытые баги.
- Не трогать stayer routes — они уже
.disabledчерез traefik, локальный доступ через:8090/:8091напрямую (с:8090/8091в URL'ах это корректно, не leak).