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 <noreply@anthropic.com>feat/v1-foundation
parent
d7156ddc96
commit
a6176341e2
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`<endpoint-host>-<peer>.conf` (z. B. `10.10.90.165-laptop.conf`) statt `clusev-wireguard.conf`.
|
||||
`<endpoint-host>-<peer>.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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue