From a6176341e290c2c981c0606fb483d355b1c953d0 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 4 Jul 2026 10:25:10 +0200 Subject: [PATCH] fix(security): keep internal IPs out of shipped code and docs/ out of the public image Audit leak-hygiene findings: - The operator's real internal dev-VM/WireGuard IPs (10.10.90.136/.162/.165) were shipped in vite.config.js (hmr.host), lang hint/placeholder strings, a code comment and the CHANGELOG. vite HMR host is now env-driven (VITE_HMR_HOST, dev .env only); all other occurrences are replaced with RFC5737 documentation addresses (203.0.113.x). Tests updated in lockstep. - docs/ is export-ignored from the git tree and named in promote.sh's refuse-to-publish list, but was never .dockerignore'd, so `COPY . .` baked the private docs (with private host/URL refs) into the public image, which the git-tree leak-guard never inspects. Added docs/ (and art/) to .dockerignore. - /update.log (new repo-root update transcript) gitignored. Co-Authored-By: Claude Opus 4.8 --- .dockerignore | 7 ++++++- .gitignore | 4 ++++ CHANGELOG.md | 4 ++-- app/Livewire/Wireguard/Index.php | 2 +- app/Services/FleetService.php | 2 +- lang/de/modals.php | 2 +- lang/de/wireguard.php | 4 ++-- lang/en/modals.php | 2 +- lang/en/wireguard.php | 4 ++-- tests/Feature/DashboardAuditTest.php | 2 +- tests/Feature/WireguardPageTest.php | 4 ++-- vite.config.js | 6 ++++-- 12 files changed, 27 insertions(+), 16 deletions(-) diff --git a/.dockerignore b/.dockerignore index 0a21a89..d8bc880 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,7 +23,10 @@ storage/framework/sessions/* storage/framework/views/* database/*.sqlite -# Dev tooling / tests / docs (not needed at runtime) +# Dev tooling / tests / docs (not needed at runtime). NOTE: docs/ is internal-only (export-ignored +# from the public git tree AND named in promote.sh's refuse-to-publish list) — it MUST also be kept +# out of the built image, or `COPY . .` bakes the private docs (with private host/URL refs) into the +# public GHCR image, which the git-tree leak-guard never inspects. Likewise art/ (README assets only). tests phpunit.xml .phpunit.cache @@ -33,6 +36,8 @@ kickoff-prompt.md rules.md CLAUDE.md README.md +docs +art # Dev-only release bridge — must NOT bake into the PUBLIC prod image (it carries the private # upstream references). Also export-ignored from the public repo. Dev keeps it via the bind-mount; diff --git a/.gitignore b/.gitignore index b07266d..0920a14 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,10 @@ yarn-error.log /run/* !/run/.gitkeep +# host-side update transcript (written at the repo root — NOT under ./run — so a compromised +# container cannot symlink-redirect the root updater's tee; see docker/restart-sentinel/watch.sh) +/update.log + # internal / AI-collaboration docs — kept on the dev box, never committed, mirrored or shipped /CLAUDE.md /rules.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c62b05..3e37f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -213,14 +213,14 @@ _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `v ein ausgewogenes Maß (~240 px) reduziert — weiterhin pixelgenau (`crispEdges`) auf weißem Rahmen, damit das Scannen am Handy zuverlässig bleibt. - **Download-Dateiname als Tunnel-Name.** Die heruntergeladene Client-Config heißt jetzt - `-.conf` (z. B. `10.10.90.165-laptop.conf`) statt `clusev-wireguard.conf`. + `-.conf` (z. B. `203.0.113.10-laptop.conf`) statt `clusev-wireguard.conf`. WireGuard-Apps benennen einen Tunnel nach der Datei, sodass der Tunnel sinnvoll vorbenannt ist. (Beim QR-Scan fragt die WireGuard-App selbst nach einem Namen — das gibt der QR-Standard nicht her; ein Hinweis im Einmal-Dialog erklärt beide Wege.) ### Behoben - **Endpoint ohne Port machte jede Client-Config ungültig.** Wurde beim Einrichten/Ändern nur ein - Host ohne Port eingegeben (z. B. `10.10.90.165` statt `10.10.90.165:51820`), landete dieser + Host ohne Port eingegeben (z. B. `203.0.113.10` statt `203.0.113.10:51820`), landete dieser Wert unverändert in den Peer-Configs — WireGuard verlangt aber zwingend `host:port`, sodass die WireGuard-App den (QR- oder Datei-)Import als „keine gültige WireGuard-Konfiguration" ablehnte. Der Listen-Port wird jetzt automatisch ergänzt, wenn der Endpoint keinen Port enthält (sowohl bei diff --git a/app/Livewire/Wireguard/Index.php b/app/Livewire/Wireguard/Index.php index 49db603..3794edb 100644 --- a/app/Livewire/Wireguard/Index.php +++ b/app/Livewire/Wireguard/Index.php @@ -382,7 +382,7 @@ class Index extends Component /** * Download the show-once client config as a .conf file. The filename becomes the tunnel name when * imported (WireGuard apps name a tunnel after the file), so build it from the endpoint host + peer - * name — e.g. "10.10.90.165-laptop.conf". (A QR scan can't carry a name; the app prompts for one.) + * name — e.g. "203.0.113.10-laptop.conf". (A QR scan can't carry a name; the app prompts for one.) */ public function downloadConfig(): ?StreamedResponse { diff --git a/app/Services/FleetService.php b/app/Services/FleetService.php index 3134114..14e9017 100644 --- a/app/Services/FleetService.php +++ b/app/Services/FleetService.php @@ -720,7 +720,7 @@ class FleetService { $ips = []; foreach ($this->lines($addr) as $line) { - // "2: ens18 inet 10.10.90.162/24 brd ..." + // "2: ens18 inet 203.0.113.11/24 brd ..." if (preg_match('/^\d+:\s+(\S+)\s+inet\s+([\d.]+)/', $line, $m)) { $ips[$m[1]][] = $m[2]; } diff --git a/lang/de/modals.php b/lang/de/modals.php index 3272aa7..85ad1b1 100644 --- a/lang/de/modals.php +++ b/lang/de/modals.php @@ -26,7 +26,7 @@ return [ 'name_label' => 'Name', 'name_placeholder' => 'z. B. Prod-Web-01', 'ip_host_label' => 'IP/Host', - 'ip_host_placeholder' => '10.10.90.10 oder host.example.com', + 'ip_host_placeholder' => '203.0.113.10 oder host.example.com', 'ssh_port_label' => 'SSH-Port', 'user_label' => 'Benutzer', 'auth_label' => 'Authentifizierung', diff --git a/lang/de/wireguard.php b/lang/de/wireguard.php index 32a8195..e29566c 100644 --- a/lang/de/wireguard.php +++ b/lang/de/wireguard.php @@ -69,7 +69,7 @@ return [ 'ssh_lock_off_title' => 'SSH-Sperre aufheben?', 'ssh_lock_off_body' => 'Port 22 wird danach wieder normal erreichbar (vorbehaltlich einer Cloud-/Host-Firewall).', 'endpoint_label' => 'Öffentlicher Endpoint (Host)', - 'endpoint_hint' => 'Nur den Host eintragen (z. B. 10.10.90.165) — der Port kommt automatisch vom Listen-Port. Einen abweichenden Port nur bei Portweiterleitung angeben (host:port). Betrifft nur künftige Peer-Configs.', + 'endpoint_hint' => 'Nur den Host eintragen (z. B. 203.0.113.10) — der Port kommt automatisch vom Listen-Port. Einen abweichenden Port nur bei Portweiterleitung angeben (host:port). Betrifft nur künftige Peer-Configs.', 'dns_label' => 'DNS-Server', 'dns_hint' => 'DNS für neue Peer-Configs (z. B. dein eigenes Gateway 10.0.0.1). Mehrere durch Komma. Betrifft nur künftige Clients.', 'dns_invalid' => 'Ungültige DNS-Adresse(n) — nur IPv4, kommagetrennt.', @@ -89,7 +89,7 @@ return [ 'setup_subnet' => 'Subnetz', 'setup_port' => 'Listen-Port (UDP)', 'setup_endpoint' => 'Öffentlicher Endpoint (optional)', - 'setup_endpoint_hint' => 'Leer = automatisch erkannte öffentliche IP. Sonst nur den Host (z. B. 10.10.90.165) — der Port kommt automatisch vom Listen-Port.', + 'setup_endpoint_hint' => 'Leer = automatisch erkannte öffentliche IP. Sonst nur den Host (z. B. 203.0.113.10) — der Port kommt automatisch vom Listen-Port.', 'setup_peer' => 'Name des ersten Clients', 'setup_submit' => 'Einrichten', 'no_host_response' => 'Keine Antwort vom Host — der WireGuard-Host-Dienst läuft nicht. Stack aktualisieren (Version-Seite) und erneut versuchen.', diff --git a/lang/en/modals.php b/lang/en/modals.php index 312579d..10460f4 100644 --- a/lang/en/modals.php +++ b/lang/en/modals.php @@ -25,7 +25,7 @@ return [ 'name_label' => 'Name', 'name_placeholder' => 'e.g. Prod-Web-01', 'ip_host_label' => 'IP/Host', - 'ip_host_placeholder' => '10.10.90.10 or host.example.com', + 'ip_host_placeholder' => '203.0.113.10 or host.example.com', 'ssh_port_label' => 'SSH port', 'user_label' => 'User', 'auth_label' => 'Authentication', diff --git a/lang/en/wireguard.php b/lang/en/wireguard.php index 716333d..0610878 100644 --- a/lang/en/wireguard.php +++ b/lang/en/wireguard.php @@ -69,7 +69,7 @@ return [ 'ssh_lock_off_title' => 'Unlock SSH?', 'ssh_lock_off_body' => 'Port 22 becomes reachable normally again (subject to any cloud/host firewall).', 'endpoint_label' => 'Public endpoint (host)', - 'endpoint_hint' => 'Enter just the host (e.g. 10.10.90.165) — the port comes from the listen port automatically. Only set a different port for port-forwarding (host:port). Affects only future peer configs.', + 'endpoint_hint' => 'Enter just the host (e.g. 203.0.113.10) — the port comes from the listen port automatically. Only set a different port for port-forwarding (host:port). Affects only future peer configs.', 'dns_label' => 'DNS server', 'dns_hint' => 'DNS for new peer configs (e.g. your own gateway 10.0.0.1). Multiple comma-separated. Only affects future clients.', 'dns_invalid' => 'Invalid DNS address(es) — IPv4 only, comma-separated.', @@ -89,7 +89,7 @@ return [ 'setup_subnet' => 'Subnet', 'setup_port' => 'Listen port (UDP)', 'setup_endpoint' => 'Public endpoint (optional)', - 'setup_endpoint_hint' => 'Empty = auto-detected public IP. Otherwise just the host (e.g. 10.10.90.165) — the port comes from the listen port automatically.', + 'setup_endpoint_hint' => 'Empty = auto-detected public IP. Otherwise just the host (e.g. 203.0.113.10) — the port comes from the listen port automatically.', 'setup_peer' => 'First client name', 'setup_submit' => 'Set up', 'no_host_response' => 'No response from the host — the WireGuard host service is not running. Update the stack (Versions page) and try again.', diff --git a/tests/Feature/DashboardAuditTest.php b/tests/Feature/DashboardAuditTest.php index d699f02..d712190 100644 --- a/tests/Feature/DashboardAuditTest.php +++ b/tests/Feature/DashboardAuditTest.php @@ -16,7 +16,7 @@ class DashboardAuditTest extends TestCase public function test_dashboard_recent_events_use_readable_labels(): void { $this->actingAs(User::factory()->create(['must_change_password' => false])); - AuditEvent::create(['actor' => 'Administrator', 'action' => 'wg.set-endpoint', 'target' => '10.10.90.165', 'ip' => '127.0.0.1']); + AuditEvent::create(['actor' => 'Administrator', 'action' => 'wg.set-endpoint', 'target' => '203.0.113.10', 'ip' => '127.0.0.1']); Livewire::test(Dashboard::class) ->assertSee('WireGuard-Endpoint geändert') // readable label diff --git a/tests/Feature/WireguardPageTest.php b/tests/Feature/WireguardPageTest.php index e8bb994..e9a4e8a 100644 --- a/tests/Feature/WireguardPageTest.php +++ b/tests/Feature/WireguardPageTest.php @@ -270,10 +270,10 @@ class WireguardPageTest extends TestCase { // The .conf filename becomes the tunnel name on import — build it from endpoint host + peer. Livewire::test(Index::class) - ->set('resultConfig', "[Interface]\nPrivateKey = x\n\n[Peer]\nEndpoint = 10.10.90.165:51820") + ->set('resultConfig', "[Interface]\nPrivateKey = x\n\n[Peer]\nEndpoint = 203.0.113.10:51820") ->set('resultName', 'laptop') ->call('downloadConfig') - ->assertFileDownloaded('10.10.90.165-laptop.conf'); + ->assertFileDownloaded('203.0.113.10-laptop.conf'); } public function test_download_config_falls_back_to_a_default_filename(): void diff --git a/vite.config.js b/vite.config.js index 45d8859..2fa3389 100644 --- a/vite.config.js +++ b/vite.config.js @@ -16,9 +16,11 @@ export default defineConfig({ port: 5173, strictPort: true, cors: true, - // Remote HMR: hot file + websocket point at the VM, not 0.0.0.0/localhost. + // Remote HMR: hot file + websocket point at the dev VM, not 0.0.0.0/localhost. The host is + // env-driven (VITE_HMR_HOST) so no private/internal address is committed or baked into the + // shipped image; set it in the dev .env only. Dev-only — prod builds assets, no HMR server. hmr: { - host: '10.10.90.136', + host: process.env.VITE_HMR_HOST || 'localhost', }, // The repo is rooted in $HOME — keep the watcher off heavy/irrelevant trees. watch: {