CluPilotCloud/config/admin_access.php

183 lines
8.2 KiB
PHP

<?php
return [
/*
| Hostnames the operator console (/admin) may be served on.
|
| The Proxmox hosts and the admin console must never be publicly reachable.
| The primary control belongs in the reverse proxy (only expose the public
| hostnames); this is defence in depth INSIDE the app, so a proxy
| misconfiguration cannot silently expose the console on a public domain.
|
| Comma-separated in ADMIN_HOSTS. EMPTY = no host restriction (dev default),
| so nobody gets locked out by simply upgrading.
|
| Example (production):
| ADMIN_HOSTS=admin.clupilot.com
*/
// Lowercased: DNS names are case-insensitive and Symfony's getHost() always
// returns lowercase, so ADMIN_HOSTS=Admin.Example.com must still match.
'hosts' => array_values(array_filter(array_map(
fn ($host) => strtolower(trim($host)),
explode(',', (string) env('ADMIN_HOSTS', ''))
))),
/*
| Whether the console's hostname serves ONLY the console.
|
| Off by default, and deliberately separate from having a hostname at all: a
| development machine lists its own IP in ADMIN_HOSTS so the console works
| without DNS, and that same address still has to serve the portal. Switch
| it on only where the console really has a hostname to itself.
*/
'exclusive' => (bool) env('ADMIN_HOST_EXCLUSIVE', false),
/*
| Hostname the public status page lives on, e.g. status.clupilot.com.
|
| Empty means it stays on every host, as it does today. Set, it becomes the
| ONLY place /status answers — everywhere else redirects there, so a link
| that already exists keeps working instead of hitting the 404 that strict
| separation would otherwise produce.
*/
'status_host' => strtolower(trim((string) env('STATUS_HOST', ''))),
/*
| Hostname the customer portal lives on, e.g. app.clupilot.com.
|
| Empty means the portal answers on every host, which is the default and
| what every development machine reached by a bare IP depends on.
|
| Set, every portal route — sign-in, registration, the dashboard, Fortify's
| own POST actions through fortify.domain — is bound to this host and does
| not exist anywhere else. www.clupilot.com/dashboard is then a 404 rather
| than a working page, which is the point: a route belongs to a hostname,
| and a route that is not registered for the host being asked cannot answer.
|
| The first attempt at this bound only the landing page and left the portal
| host-agnostic — so the website vanished from the portal, and the portal
| stayed reachable from the website. Half a separation is no separation.
*/
'app_host' => strtolower(trim((string) env('APP_HOST', ''))),
/*
| Hostname für Dateien zum Herunterladen, z. B. files.clupilot.com.
|
| Eine eigene Adresse, weil hier zweierlei liegt, das dieselben Ansprüche
| hat: öffentlich, dauerhaft zitierbar, immer erreichbar. Rechtsdokumente
| (AGB, AV, TOM) stehen in Verträgen und Rechnungen und müssen Jahre halten
| — eine Adresse, die mit einem Umbau des Portals wandert, taugt dafür
| nicht. Und das Bootstrap-Archiv, das ein Server im Rettungssystem holt,
| bevor er irgendetwas anderes kann.
|
| Zwei Bereiche auf diesem Hostnamen, mit entgegengesetzten Regeln:
|
| - ÖFFENTLICH: `storage/app/files/public/`. Anonym abrufbar, indexierbar.
| Wer die AGB sucht, soll sie finden.
| - PRIVAT: `/bootstrap.tar.gz`, nur mit gültigem Einmal-Code, und ohne den
| ein 404 statt eines 403 — dieselbe Regel wie bei der Konsole: ein
| Fremder soll nicht erfahren, dass es das hier gibt. Das Archiv liegt in
| keinem Verzeichnis; es wird bei Bedarf aus `deploy/bootstrap` gebaut.
|
| Leer heißt: das Archiv bleibt auf dem Portal-Hostnamen erreichbar, wie
| bisher. So bricht nichts, solange die DNS-Einträge für den neuen Namen
| noch nicht stehen.
*/
'files_host' => strtolower(trim((string) env('FILES_HOST', ''))),
/*
| Hostnames the public website lives on, e.g. www.clupilot.com,clupilot.com.
|
| Comma-separated, and the FIRST is canonical: it serves the site, and every
| other name in the list redirects to it permanently, path and all. An apex
| domain and its www are two names for one site, and answering on both
| without picking one splits the search rankings and the cookies between
| them.
|
| Empty means the landing page answers on every host, which is what it did
| — including on the portal's own hostname. Somebody who typed
| app.clupilot.com got the shop window: marketing copy, a pricing table and
| a "sign up" call to action, at the address where their servers are.
|
| Set, the website — landing page, robots.txt and the legal pages — exists
| on these names and nowhere else. Every other host answers "/" with the
| product instead. The console keeps its own "/": its routes are registered
| first and win.
|
| Binding the legal pages is also what makes route('legal.impressum')
| produce a canonical URL from inside a queued mail, where there is no
| request to take a hostname from.
*/
'site_hosts' => array_values(array_filter(array_map(
fn ($host) => strtolower(trim($host)),
explode(',', (string) env('SITE_HOST', ''))
))),
/*
| Key for VPN configs stored at rest (32 bytes, base64). Separate from
| APP_KEY on purpose — see App\Services\Wireguard\ConfigVault. Empty means
| storing configs is switched off, and the console says so rather than
| quietly writing credentials in the clear.
|
| Generate with: head -c 32 /dev/urandom | base64
*/
'vpn_config_key' => env('VPN_CONFIG_KEY', ''),
/*
| The hostname the console answers on INSIDE the tunnel.
|
| Set, the VPN gateway and resolver are started (compose profile `vpn`) and
| issued client configs point their DNS at the hub. Empty, none of that
| exists and the VPN is only a management network — which is what an
| installation without the extra services should be.
|
| Deliberately the SAME name the console already uses publicly: the
| certificate is bound to the name, not to the address serving it, so
| nothing new has to be issued and nothing about the internal network ends
| up in a public DNS zone.
*/
'vpn_internal_host' => strtolower(trim((string) env('VPN_INTERNAL_HOST', ''))),
/*
| Whether the tunnel-side gateway is actually READY, not merely wanted.
|
| The hostname alone is not enough: the deployment refuses to start the
| gateway until a certificate for it has been found, and a client config
| that names a resolver nobody started takes the device's whole name
| resolution down for as long as the tunnel is up. The certificate path is
| written by the same step that starts the services, so it is the honest
| signal.
*/
'vpn_ready' => strtolower(trim((string) env('VPN_INTERNAL_HOST', ''))) !== ''
&& filter_var(env('VPN_READY', false), FILTER_VALIDATE_BOOLEAN),
/*
| Key for credentials stored in the console (32 bytes, base64).
|
| Separate from APP_KEY on purpose: rotating APP_KEY is ordinary
| maintenance, and it would otherwise render every stored credential
| unreadable — discovered when Stripe stops answering. Empty means storing
| credentials is switched off, and the console says so rather than falling
| back to a key that gets rotated.
|
| Generate with: head -c 32 /dev/urandom | base64
*/
'secrets_key' => env('SECRETS_KEY', ''),
/*
| Networks that count as "us" while the public site is hidden
| (PublicSiteGate). The WireGuard management subnet by default, so anyone on
| the VPN sees the real site while the outside world sees a placeholder.
|
| Note this is compared against the CLIENT address, which is only correct
| because TrustProxies is configured — behind an untrusted proxy every
| request would look like it came from the proxy.
*/
'trusted_ranges' => array_values(array_filter(array_map(
'trim',
explode(',', (string) env('TRUSTED_RANGES', '10.66.0.0/24,127.0.0.1')),
))),
];