From 22603d6ee90a688ee5776133611d2531360e4fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Wed, 22 Jul 2026 17:15:31 +0200 Subject: [PATCH] fix(nginx): przepusc /.well-known/ (discovery OAuth) mimo blokady plikow ukrytych MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reguła `location ~ /\.` blokujaca .git/.env przechwytywala takze /.well-known/ i zwracala 403. Regexy w nginksie maja pierwszenstwo przed zwyklymi prefiksami, wiec zaden prefiksowy location nie mogl jej wyprzedzic. Skutek: metadane serwera autoryzacji OAuth (RFC 8414, /.well-known/oauth-authorization-server) byly nieosiagalne z zewnatrz. Klient MCP `bpp-mcp` nie mogl przeprowadzic discovery i logowanie padalo, mimo ze /o/authorize/, /o/token/ i /o/register/ dzialaly poprawnie. Naprawa: `location ^~ /.well-known/` — modyfikator `^~` stawia prefiks ponad regexami. Django odpowiada 404 na nieznane sciezki .well-known, wiec nic sie nie odslania, a .git/.env dalej lapie regex. ACME (Let's Encrypt) nie byl dotkniety: blok port-80 w vhost.conf.template nie includuje _bpp-locations.conf, wiec walidacja HTTP-01 dzialala. Test 15d w tests/test_makefile.sh pilnuje obu stron kontraktu naraz: /.well-known/oauth-authorization-server musi dojsc do appservera, a /.git/config i /.env musza dalej dostawac 403 — zeby "naprawa" polegajaca na skasowaniu blokady plikow ukrytych nie przeszla jako zielona. Zweryfikowane na nginx:1.30.2: przed poprawka 15d pada (403), po poprawce 22/22 zielone, testy ACME bez zmian. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_019D6Zytbsj8xjZCx8etyMeN --- defaults/webserver/_bpp-locations.conf | 18 +++++++++++ tests/test_makefile.sh | 43 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/defaults/webserver/_bpp-locations.conf b/defaults/webserver/_bpp-locations.conf index ddad74b..74738fc 100644 --- a/defaults/webserver/_bpp-locations.conf +++ b/defaults/webserver/_bpp-locations.conf @@ -340,6 +340,24 @@ location = /netdata { # SECURITY BLOCKS # ============================================================================ +# WYJATEK PRZED blokada plikow ukrytych: /.well-known/ (RFC 8615). +# To standardowa przestrzen metadanych serwisu, a nie "plik ukryty" — leza tam +# m.in. metadane serwera autoryzacji OAuth (RFC 8414, +# /.well-known/oauth-authorization-server), po ktore siega kazdy klient MCP +# przed logowaniem, a takze security.txt. +# +# UWAGA na kolejnosc matchowania location w nginksie: regex `~` ma +# pierwszenstwo przed zwyklym prefiksem, wiec blok `location ~ /\.` ponizej +# przechwytywal ZADANIA do /.well-known/ i zwracal 403 (discovery OAuth padalo, +# `bpp-mcp login` nie mial skad wziac authorization_endpoint). Modyfikator `^~` +# stawia ten prefiks PONAD regexami i przywraca poprawne zachowanie. +# Django odpowiada 404 na nieznane sciezki .well-known, wiec nic sie nie +# odslania; .git/.env dalej lapie regex ponizej. +location ^~ /.well-known/ { + limit_req zone=bpp_general burst=100 nodelay; + try_files $uri @proxy_to_app; +} + # Block access to hidden files and directories # Prevents access to .git, .env, and other hidden files location ~ /\. { diff --git a/tests/test_makefile.sh b/tests/test_makefile.sh index cb63ecb..3994dd3 100755 --- a/tests/test_makefile.sh +++ b/tests/test_makefile.sh @@ -957,6 +957,49 @@ PYEOF fail "GET /admin/: oczekiwane 301, otrzymano '$code'" fi + _runtime_stop_nginx + + # ==== 15d: /.well-known/ przechodzi na HTTPS, ukryte pliki dalej blokowane ==== + # Regresja, ktora juz raz wystapila na produkcji: `location ~ /\.` (blokada + # plikow ukrytych) to REGEX, a regexy w nginksie maja pierwszenstwo przed + # zwyklymi prefiksami — wiec przechwytywal /.well-known/ i zwracal 403. + # Skutek: metadane serwera autoryzacji OAuth (RFC 8414) byly nieosiagalne i + # discovery klienta MCP padalo przed logowaniem. Lekarstwo to modyfikator + # `^~`, ktory stawia prefiks PONAD regexami. + # + # Ten test pilnuje OBU stron kontraktu naraz — samo "przepusc .well-known" + # dalo by sie spelnic kasujac blokade plikow ukrytych, co byloby regresja + # bezpieczenstwa. Dlatego .git/.env musza dalej dostawac 403. + yellow " -- 15d: /.well-known/ (OAuth discovery) vs blokada plikow ukrytych --" + start_out=$(_runtime_start_nginx "" "legacy.example.org") || { + fail "well-known-test nginx nie wstal w 15s" + return + } + read -r nginx_cid port_80 port_443 <<< "$start_out" + + # Metadane AS MUSZA dojsc do Django (nie 403). Appserver echo-uje Path, + # wiec sprawdzamy takze, ze sciezka dolecila w calosci — samo 200 moglo by + # pochodzic z przypadkowego statycznego pliku. + body=$(curl -sk --resolve "legacy.example.org:$port_443:127.0.0.1" \ + "https://legacy.example.org:$port_443/.well-known/oauth-authorization-server" || true) + if echo "$body" | grep -q "Path: /.well-known/oauth-authorization-server"; then + pass "HTTPS /.well-known/oauth-authorization-server -> proxy do appservera" + else + fail "well-known OAuth: oczekiwano proxy, otrzymano: $(echo "$body" | head -c 200)" + fi + + # Blokada plikow ukrytych MUSI przetrwac wyjatek na .well-known. + for hidden in "/.git/config" "/.env"; do + code=$(curl -sk -o /dev/null -w '%{http_code}' \ + --resolve "legacy.example.org:$port_443:127.0.0.1" \ + "https://legacy.example.org:$port_443$hidden" || true) + if [ "$code" = "403" ]; then + pass "HTTPS $hidden -> 403 (blokada plikow ukrytych dziala)" + else + fail "$hidden: oczekiwane 403, otrzymano '$code' (blokada oslabiona!)" + fi + done + # cleanup via trap RETURN }