Show container-console route for the setup token (#2302)

The unlock screen told Docker users to run `docker exec` from the host,
which does not fit the Unraid, Portainer and TrueNAS app UIs, where the
console already runs inside the container. Those `container_other`
installs are the largest group stalling before first setup: in the week
to 2026-09-28, 158 of 3114 persistent Docker installs never got past
first start, against 94 of 5462 binary and systemd installs.

The startup log also suggested revealing the token "by reading the token
file path". That file holds ciphertext, as the security-privacy contract
requires, so anyone following the hint pasted JSON the unlock screen
rejects. The log now names the command, including the console form, and
the install docs and the contract gain the same guidance.

Contract-Neutral: copy-only: new unlock-screen command row and corrected
startup log text; no API payload, lifecycle, storage or primitive
change; owning security-privacy clause updated
This commit is contained in:
rcourtman
2026-09-28 16:04:51 +01:00
committed by GitHub
10 changed files with 57 additions and 17 deletions
+1
View File
@@ -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 <pod> -- /app/pulse bootstrap-token` |
| **Systemd** | `sudo pulse bootstrap-token` |
| **Proxmox LXC** | `pct exec <ctid> -- /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) |
@@ -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
+31 -16
View File
@@ -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."
}
+1
View File
@@ -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 <pod> -- /app/pulse bootstrap-token` |
| **Systemd** | `sudo pulse bootstrap-token` |
| **Proxmox LXC** | `pct exec <ctid> -- /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) |
@@ -117,6 +117,10 @@ describe('WelcomeStep', () => {
expect(
screen.getByText('docker exec <pulse-container> /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(
@@ -37,6 +37,13 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
label: t('setup.welcome.deploymentLabel.docker'),
command: 'docker exec <pulse-container> /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'),
+2
View File
@@ -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 <pulse-container> 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',
+2
View File
@@ -309,6 +309,8 @@ export const ES_MESSAGE_OVERRIDES = {
'Pulse parece ejecutarse en Docker. Ejecuta el comando en el host Docker y reemplaza <pulse-container> 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',
+3
View File
@@ -301,6 +301,8 @@ export const EN_MESSAGES = {
'Pulse appears to be running in Docker. Run the command on the Docker host and replace <pulse-container> 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',
+1 -1
View File
@@ -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 <container> /app/pulse bootstrap-token`, or `/app/pulse bootstrap-token` from the container console)")
} else {
logger.Info().
Str("token_path", path).