diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 29db86d15b..37e49db8d7 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -216,6 +216,7 @@ Pulse is secure by default. On first launch, you must retrieve a **Bootstrap Tok | Platform | Command | |----------|---------| | **Docker** | `docker exec pulse /app/pulse bootstrap-token` | +| **Docker app UIs** (Unraid, Portainer, TrueNAS apps) | Open the Pulse container's console and run `/app/pulse bootstrap-token` | | **Kubernetes** | `kubectl exec -it -- /app/pulse bootstrap-token` | | **Systemd** | `sudo pulse bootstrap-token` | | **Proxmox LXC** | `pct exec -- /usr/local/bin/pulse bootstrap-token` (run on the Proxmox host; the installer prints this command with your container ID at the end of the install) | diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 2f1fba1842..d59d784af3 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -2057,6 +2057,11 @@ plaintext secret file on disk. Canonical runtime persistence must keep the token encrypted at rest, and any legacy plaintext bootstrap-token file must be treated only as migration input that is rewritten immediately into the encrypted canonical format on load. +Operator guidance for recovering that token, in the startup log and on the +first-run unlock screen, must name the supported command and never the token +file, because the file holds ciphertext. The unlock screen lists the host, +`docker exec`, in-container console (`/app/pulse bootstrap-token`) and +`pct exec` forms without revealing the server's deployment details. Managed first-session proof may reset that boundary only through the dev-only `/api/security/dev/reset-first-run` route under authenticated `settings:write`; harnesses may not scrape `.env`, delete persisted token diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 42a4f46e5d..649b9912d5 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,28 +1,43 @@ { "version": 1, - "base_sha": "6d7e3342e56a4f8f2b411acb3df8dc7777d4ec7f", - "verified_at": "2026-09-27T23:52:13Z", + "base_sha": "75c72ef505b24e31214162b7b46f97e357528ae3", + "verified_at": "2026-09-28T14:14:00Z", "result": "passed", - "changed_paths": ["frontend-modern/src/api/settings.ts"], + "changed_paths": [ + "frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx", + "frontend-modern/src/i18n/messages.de.ts", + "frontend-modern/src/i18n/messages.es.ts", + "frontend-modern/src/i18n/messages.ts" + ], "content_sha256": { - "frontend-modern/src/api/settings.ts": "4172b94c268be6a0ea000e73d155e540f11776e1c59325250e13716722be5aeb" + "frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx": "cb25935ec49782592c1602ae1502f5becc94108d61c3a786bbd65654d757e540", + "frontend-modern/src/i18n/messages.de.ts": "ca6bd4a42cac62c3bdec6ef25c35b5ec78b886d0455c6eab0ae8f154c0fd3361", + "frontend-modern/src/i18n/messages.es.ts": "5b3867658b39164afdece84e7a1eb28195b18d810d9ca9f5ed6761a54e274dcd", + "frontend-modern/src/i18n/messages.ts": "9e0bd2b4addcf984e499499460fee59f705d684cffb4fba1d7da0361cfbbdd31" }, - "routes": ["/tmp/telemetry-schema-2285.html"], + "routes": [ + "/ (first-run unlock screen, fresh container)" + ], "viewports": [ - {"width": 1280, "height": 800}, - {"width": 390, "height": 844} + { + "width": 1280, + "height": 800 + }, + { + "width": 390, + "height": 844 + } ], "states": [ - "Production GeneralSettingsPanel mounted in an isolated Vite fixture with a synthetic schema-v18 telemetry-preview API response", - "Preview payload displayed stable-channel skipped, metadata_error, and available outcomes with corresponding false, false, and true update_available booleans", - "The payload block stayed inside the card; the metadata_error JSON scrolled within the block at 390px without page-level horizontal overflow", - "Desktop and phone screenshots for all three outcomes were inspected for placement, clipping and scrolling" + "Fresh rcourtman/pulse:latest (v6.4.1) container in first-run state, serving this branch's vite build via PULSE_FRONTEND_DIR, reached through an SSH tunnel to localhost", + "Unlock setup lists four rows: direct host, Docker deployment, the new Container console (Unraid, Portainer, TrueNAS apps) row with /app/pulse bootstrap-token, and LXC", + "At 390px the new label wraps to two lines and the command truncates like the existing rows, with no page-level horizontal overflow (scrollWidth 390)", + "At 1280px all four commands render in full with no overflow" ], "interactions": [ - "Clicked Preview payload and then Refresh payload twice per viewport, replacing the synthetic API outcome on each request", - "Parsed the rendered payload JSON and asserted schema_version, update_channel, update_check_outcome and update_available", - "Checked page and preview scroll widths at both viewports and checked for uncaught page errors" + "Clicked the copy button on the Container console row and saw the Command copied to clipboard toast and the check icon", + "Ran /app/pulse bootstrap-token from a shell inside the container as root and as the pulse user and confirmed it printed the token" ], - "command": "pulse-worker-browser tmp/browser-proof/telemetry-schema-2285.cjs (Playwright 1.56.1, Chromium 141.0.7390.37)", - "notes": "Offline production-component fixture with synthetic API data, not a full application login, installed sender, receiver or release acceptance. The browser result and six screenshots are retained under tmp/browser-proof in this assigned workspace." + "command": "Claude desktop browser pane against a pulse-dev probe container", + "notes": "Backend was the published v6.4.1 image, not a build of this branch. The frontend is this branch. Telemetry was disabled on the probe containers." } diff --git a/frontend-modern/public/docs/INSTALL.md b/frontend-modern/public/docs/INSTALL.md index 29db86d15b..37e49db8d7 100644 --- a/frontend-modern/public/docs/INSTALL.md +++ b/frontend-modern/public/docs/INSTALL.md @@ -216,6 +216,7 @@ Pulse is secure by default. On first launch, you must retrieve a **Bootstrap Tok | Platform | Command | |----------|---------| | **Docker** | `docker exec pulse /app/pulse bootstrap-token` | +| **Docker app UIs** (Unraid, Portainer, TrueNAS apps) | Open the Pulse container's console and run `/app/pulse bootstrap-token` | | **Kubernetes** | `kubectl exec -it -- /app/pulse bootstrap-token` | | **Systemd** | `sudo pulse bootstrap-token` | | **Proxmox LXC** | `pct exec -- /usr/local/bin/pulse bootstrap-token` (run on the Proxmox host; the installer prints this command with your container ID at the end of the install) | diff --git a/frontend-modern/src/components/SetupWizard/__tests__/WelcomeStep.test.tsx b/frontend-modern/src/components/SetupWizard/__tests__/WelcomeStep.test.tsx index 96ff7f8e66..847540aa52 100644 --- a/frontend-modern/src/components/SetupWizard/__tests__/WelcomeStep.test.tsx +++ b/frontend-modern/src/components/SetupWizard/__tests__/WelcomeStep.test.tsx @@ -117,6 +117,10 @@ describe('WelcomeStep', () => { expect( screen.getByText('docker exec /app/pulse bootstrap-token'), ).toBeInTheDocument(); + expect( + screen.getByText('Container console (Unraid, Portainer, TrueNAS apps)'), + ).toBeInTheDocument(); + expect(screen.getByText('/app/pulse bootstrap-token')).toBeInTheDocument(); // Absolute path, because pct exec runs with // PATH=/sbin:/bin:/usr/sbin:/usr/bin and cannot resolve /usr/local/bin. expect( diff --git a/frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx b/frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx index 0e31b32e9e..281ab5b938 100644 --- a/frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx +++ b/frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx @@ -37,6 +37,13 @@ export const WelcomeStep: Component = (props) => { label: t('setup.welcome.deploymentLabel.docker'), command: 'docker exec /app/pulse bootstrap-token', }, + { + // Unraid, Portainer and TrueNAS open a shell inside the container + // itself, so the docker exec prefix above does not apply there. + id: 'container-console', + label: t('setup.welcome.deploymentLabel.containerConsole'), + command: '/app/pulse bootstrap-token', + }, { id: 'lxc', label: t('setup.welcome.deploymentLabel.lxc'), diff --git a/frontend-modern/src/i18n/messages.de.ts b/frontend-modern/src/i18n/messages.de.ts index 82332ac3d3..164491e1d1 100644 --- a/frontend-modern/src/i18n/messages.de.ts +++ b/frontend-modern/src/i18n/messages.de.ts @@ -317,6 +317,8 @@ export const DE_MESSAGE_OVERRIDES = { 'Pulse scheint in Docker zu laufen. Fuehren Sie den Befehl auf dem Docker-Host aus und ersetzen Sie durch den Namen des laufenden Pulse-Containers.', 'setup.welcome.deploymentHint.lxc': 'Pulse scheint in LXC-Container {ctid} zu laufen. Fuehren Sie den Befehl auf dem Proxmox-Host aus, um in diesen Container zu wechseln und das einmalige Setup-Token auszugeben.', + 'setup.welcome.deploymentLabel.containerConsole': + 'Container-Konsole (Unraid, Portainer, TrueNAS-Apps)', 'setup.welcome.deploymentLabel.containerized': 'Containerisierte Bereitstellung', 'setup.welcome.deploymentLabel.direct': 'Direkte Host-Installation', 'setup.welcome.deploymentLabel.docker': 'Docker-Bereitstellung', diff --git a/frontend-modern/src/i18n/messages.es.ts b/frontend-modern/src/i18n/messages.es.ts index 0131ead0ed..8af0517c74 100644 --- a/frontend-modern/src/i18n/messages.es.ts +++ b/frontend-modern/src/i18n/messages.es.ts @@ -309,6 +309,8 @@ export const ES_MESSAGE_OVERRIDES = { 'Pulse parece ejecutarse en Docker. Ejecuta el comando en el host Docker y reemplaza por el nombre del contenedor Pulse en ejecución.', 'setup.welcome.deploymentHint.lxc': 'Pulse parece ejecutarse en el contenedor LXC {ctid}. Ejecuta el comando en el host Proxmox para entrar en ese contenedor e imprimir el token de configuración de un solo uso.', + 'setup.welcome.deploymentLabel.containerConsole': + 'Consola del contenedor (Unraid, Portainer, apps de TrueNAS)', 'setup.welcome.deploymentLabel.containerized': 'Despliegue contenedorizado', 'setup.welcome.deploymentLabel.direct': 'Instalación directa en host', 'setup.welcome.deploymentLabel.docker': 'Despliegue Docker', diff --git a/frontend-modern/src/i18n/messages.ts b/frontend-modern/src/i18n/messages.ts index 8196841c77..72ff793b52 100644 --- a/frontend-modern/src/i18n/messages.ts +++ b/frontend-modern/src/i18n/messages.ts @@ -301,6 +301,8 @@ export const EN_MESSAGES = { 'Pulse appears to be running in Docker. Run the command on the Docker host and replace with the running Pulse container name.', 'setup.welcome.deploymentHint.lxc': 'Pulse appears to be running in LXC container {ctid}. Run the command on the Proxmox host to execute into that container and print the one-time setup token.', + 'setup.welcome.deploymentLabel.containerConsole': + 'Container console (Unraid, Portainer, TrueNAS apps)', 'setup.welcome.deploymentLabel.containerized': 'Containerized deployment', 'setup.welcome.deploymentLabel.direct': 'Direct host install', 'setup.welcome.deploymentLabel.docker': 'Docker deployment', @@ -694,6 +696,7 @@ export const FIRST_SESSION_MONITORING_MIGRATED_MESSAGE_KEYS = [ 'setup.welcome.deploymentHint.dockerNamed', 'setup.welcome.deploymentHint.dockerUnnamed', 'setup.welcome.deploymentHint.lxc', + 'setup.welcome.deploymentLabel.containerConsole', 'setup.welcome.deploymentLabel.containerized', 'setup.welcome.deploymentLabel.direct', 'setup.welcome.deploymentLabel.docker', diff --git a/internal/api/bootstrap_token.go b/internal/api/bootstrap_token.go index 3d4d05efb2..9c429945fb 100644 --- a/internal/api/bootstrap_token.go +++ b/internal/api/bootstrap_token.go @@ -62,7 +62,7 @@ func (r *Router) initializeBootstrapToken() { if created { logger.Warn(). Str("token_path", path). - Msg("Bootstrap setup token created on disk; reveal it locally with `pulse bootstrap-token` or by reading the token file path") + Msg("Bootstrap setup token created; the file is encrypted, so reveal the token with `pulse bootstrap-token` (Docker: `docker exec /app/pulse bootstrap-token`, or `/app/pulse bootstrap-token` from the container console)") } else { logger.Info(). Str("token_path", path).