diff --git a/php-fpm-nginx/common/default-rootless.conf b/php-fpm-nginx/common/default-rootless.conf index 0d5e700..a163a61 100644 --- a/php-fpm-nginx/common/default-rootless.conf +++ b/php-fpm-nginx/common/default-rootless.conf @@ -163,6 +163,21 @@ server { # SECURITY: Block sensitive files and directories # ───────────────────────────────────────────────────────────────────────── + # RFC 8615 — /.well-known/ is the reserved namespace for site-wide + # service metadata: OIDC discovery, OAuth authorization-server + # metadata, security.txt, ACME challenges, change-password, etc. + # Apps register handlers (e.g. Laravel: `Route::get('.well-known/...')`), + # but the catch-all dotfile block below would otherwise 404 every + # request before the app sees it. + # + # `^~` makes this a prefix match that wins over the regex catch-all + # below — nginx prefers `^~` matches and skips regex evaluation + # when one matches. Place BEFORE the `/\.` block; ordering of + # regex blocks matters but `^~` short-circuits regardless. + location ^~ /.well-known/ { + try_files $uri $uri/ /index.php?$query_string; + } + # Block hidden files (.env, .git, .htaccess, .svn, etc.) location ~ /\.(env|git|svn|htaccess|htpasswd|gitignore|gitattributes|dockerignore) { deny all; diff --git a/php-fpm-nginx/common/default.conf b/php-fpm-nginx/common/default.conf index 899a380..461fc3a 100644 --- a/php-fpm-nginx/common/default.conf +++ b/php-fpm-nginx/common/default.conf @@ -162,6 +162,21 @@ server { # SECURITY: Block sensitive files and directories # ───────────────────────────────────────────────────────────────────────── + # RFC 8615 — /.well-known/ is the reserved namespace for site-wide + # service metadata: OIDC discovery, OAuth authorization-server + # metadata, security.txt, ACME challenges, change-password, etc. + # Apps register handlers (e.g. Laravel: `Route::get('.well-known/...')`), + # but the catch-all dotfile block below would otherwise 404 every + # request before the app sees it. + # + # `^~` makes this a prefix match that wins over the regex catch-all + # below — nginx prefers `^~` matches and skips regex evaluation + # when one matches. Place BEFORE the `/\.` block; ordering of + # regex blocks matters but `^~` short-circuits regardless. + location ^~ /.well-known/ { + try_files $uri $uri/ /index.php?$query_string; + } + # Block hidden files (.env, .git, .htaccess, .svn, etc.) location ~ /\.(env|git|svn|htaccess|htpasswd|gitignore|gitattributes|dockerignore) { deny all; diff --git a/tests/e2e/scenarios/test-security.sh b/tests/e2e/scenarios/test-security.sh index 4fba49d..48a9c5e 100755 --- a/tests/e2e/scenarios/test-security.sh +++ b/tests/e2e/scenarios/test-security.sh @@ -137,6 +137,39 @@ else log_fail "composer.lock is accessible! (HTTP $CODE)" fi +# RFC 8615 — /.well-known/ MUST reach the app, NOT be denied at nginx. +# The test app may not have a route registered (→ 404 from PHP), but +# anything other than 403 means nginx forwarded it, which is what +# OIDC discovery, ACME, security.txt, etc. all rely on. A 403 here +# means the catch-all dotfile block re-broke; a 404 from nginx (the +# default error page, not Laravel's) also means it didn't reach PHP +# — distinguish by checking the response body. +CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8101/.well-known/openid-configuration" 2>/dev/null) +if [ "$CODE" = "403" ]; then + log_fail "/.well-known/openid-configuration is denied at nginx (HTTP 403) — RFC 8615 violation, OIDC discovery breaks" +else + log_success "/.well-known/openid-configuration is forwarded to app (HTTP $CODE)" +fi + +# /.well-known/ paths MUST forward to PHP — confirm via response body. +# Laravel's 404 page is HTML with Tailwind/Inertia content; nginx's +# raw 404 is the bare "
nginx
" page. If we see the +# nginx default-error body, the path was NOT forwarded. +BODY=$(curl -s "http://localhost:8101/.well-known/openid-configuration" 2>/dev/null) +if echo "$BODY" | grep -q "
nginx
"; then + log_fail "/.well-known/* hit nginx's default error page — the catch-all dotfile block is winning" +else + log_success "/.well-known/* requests reach the app (no nginx default-error body)" +fi + +# Confirm /.env STILL blocked — regression guard for the well-known fix. +CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8101/.env" 2>/dev/null) +if [ "$CODE" = "403" ] || [ "$CODE" = "404" ]; then + log_success ".env stays blocked after well-known allow (HTTP $CODE)" +else + log_fail ".env became accessible — well-known allow over-broadened the rule! (HTTP $CODE)" +fi + # ═══════════════════════════════════════════════════════════════════════════ # TEST 4: Directory Blocking # ═══════════════════════════════════════════════════════════════════════════