Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions php-fpm-nginx/common/default-rootless.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions php-fpm-nginx/common/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
33 changes: 33 additions & 0 deletions tests/e2e/scenarios/test-security.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<center>nginx</center>" 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 "<center>nginx</center>"; 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
# ═══════════════════════════════════════════════════════════════════════════
Expand Down
Loading