feat(security): keep the operator console off the public hostnames
Only the marketing site and the customer portal are public; /admin and the Proxmox hosts stay on the private network. nginx was a single server_name _ catch-all, so /admin was served on all four dev domains. Two layers: - nginx denies /admin on the known-public hostnames before PHP is reached (denylist, so an allowlist typo cannot lock the operators out). - ADMIN_HOSTS is now populated, making the app layer a strict allowlist that also covers /livewire/update, which nginx cannot attribute to /admin. Verified live against the running stack: /admin is 404 on www/app/api/ws and on any unlisted host, 302->login on admin.dev / the private IP / localhost, while / and /dashboard stay reachable everywhere. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
ba861ad579
commit
7247edf9d8
|
|
@ -164,7 +164,7 @@ MONITORING_ATTEMPTS=2
|
|||
# antwortet /admin mit 404 (nicht 403 — verrät nicht, dass es die Konsole gibt).
|
||||
# Komma-getrennt. LEER = keine Einschränkung (aktueller Dev-Stand).
|
||||
# Beispiel: ADMIN_HOSTS=admin.dev.clupilot.com,10.10.90.185
|
||||
ADMIN_HOSTS=
|
||||
ADMIN_HOSTS=admin.dev.clupilot.com,10.10.90.185,localhost,127.0.0.1
|
||||
|
||||
# ── Nextcloud blueprint template ─────────────────────────────────────────
|
||||
# NOT an env var: set the Proxmox template VMID per plan in
|
||||
|
|
|
|||
|
|
@ -1,3 +1,23 @@
|
|||
# Hostnames that are reachable from the public internet. The operator console
|
||||
# (/admin) must NEVER be served on these — only the marketing site and the
|
||||
# customer portal are public; Proxmox hosts and /admin stay on the private
|
||||
# network / VPN.
|
||||
#
|
||||
# Deliberately a denylist, not an allowlist: a typo in an allowlist would lock
|
||||
# the operators out of their own console. The app-level allowlist (ADMIN_HOSTS,
|
||||
# see config/admin_access.php) is the strict layer — this one guarantees that a
|
||||
# request arriving on a known-public hostname never even reaches PHP.
|
||||
map $host $admin_host_is_public {
|
||||
default 0;
|
||||
www.dev.clupilot.com 1;
|
||||
app.dev.clupilot.com 1;
|
||||
api.dev.clupilot.com 1;
|
||||
ws.dev.clupilot.com 1;
|
||||
# Production (add the real public hostnames here before launch):
|
||||
# www.clupilot.com 1;
|
||||
# app.clupilot.com 1;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
|
@ -7,6 +27,21 @@ server {
|
|||
charset utf-8;
|
||||
client_max_body_size 64M;
|
||||
|
||||
# ^~ beats the regex locations, so this decides before PHP is reached. The
|
||||
# try_files rewrite to /index.php re-runs location matching, so the console
|
||||
# still executes normally on a permitted host.
|
||||
#
|
||||
# This only covers direct hits: Livewire posts component actions to
|
||||
# /livewire/update, which nginx cannot attribute to /admin. That case is
|
||||
# handled in the app (RestrictAdminHost is registered as Livewire-persistent
|
||||
# middleware) — do not treat this block as the only control.
|
||||
location ^~ /admin {
|
||||
if ($admin_host_is_public) {
|
||||
return 404;
|
||||
}
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue