diff --git a/frontend-modern/src/api/__tests__/nodes.branchcov0724pm.test.ts b/frontend-modern/src/api/__tests__/nodes.branchcov0724pm.test.ts index 21200d6a7c..98b333ed27 100644 --- a/frontend-modern/src/api/__tests__/nodes.branchcov0724pm.test.ts +++ b/frontend-modern/src/api/__tests__/nodes.branchcov0724pm.test.ts @@ -149,6 +149,7 @@ describe('NodesAPI — branch coverage (downloadProxmoxSetupScript failure arms) }); const pveBootstrap = (): ProxmoxSetupCommandResponse => ({ + setupToken: 'synthetic-setup-token', type: 'pve', host: 'https://pve.example:8006', url: 'https://pulse.example/base/api/setup-script?type=pve', diff --git a/frontend-modern/src/api/__tests__/nodes.test.ts b/frontend-modern/src/api/__tests__/nodes.test.ts index 5a4602e196..bd7eadcb10 100644 --- a/frontend-modern/src/api/__tests__/nodes.test.ts +++ b/frontend-modern/src/api/__tests__/nodes.test.ts @@ -407,7 +407,7 @@ describe('NodesAPI', () => { [field]: `unsafe-${secret}`, }); await expect( - NodesAPI.getProxmoxSetupCommand({ type: 'pve', host: 'pve.example' }), + NodesAPI.getProxmoxSetupCommand({ type: 'pve', host: 'pve.example', backupPerms: false }), ).rejects.toThrow('Setup credentials must be entered separately from commands and URLs'); }, ); diff --git a/frontend-modern/src/stores/__tests__/tokenReveal.test.ts b/frontend-modern/src/stores/__tests__/tokenReveal.test.ts index 95f541110f..999705484c 100644 --- a/frontend-modern/src/stores/__tests__/tokenReveal.test.ts +++ b/frontend-modern/src/stores/__tests__/tokenReveal.test.ts @@ -48,12 +48,24 @@ describe('tokenRevealStore', () => { const state = tokenRevealStore.state(); expect(state).not.toBeNull(); expect(state!.token).toBe('secret-token'); - expect(state!.record.id).toBe('token-1'); + expect(state!.record!.id).toBe('token-1'); expect(state!.source).toBe('settings'); expect(state!.note).toBe('Test note'); expect(state!.issuedAt).toBeDefined(); }); + it('reveals a temporary setup token without inventing an API token record', () => { + showTokenReveal({ + token: 'synthetic-setup-token', + source: 'pve', + note: 'Private input only', + }); + expect(tokenRevealStore.state()?.token).toBe('synthetic-setup-token'); + expect(tokenRevealStore.state()?.record).toBeUndefined(); + dismissTokenReveal(); + expect(tokenRevealStore.state()).toBeNull(); + }); + it('sets issuedAt to current timestamp', () => { const before = Date.now(); showTokenReveal({ diff --git a/install.sh b/install.sh index affec613cb..d345f3c7ce 100755 --- a/install.sh +++ b/install.sh @@ -2349,7 +2349,7 @@ for _field_name, _value, _requires_token in command_fields: if modern: valid = "PULSE_SETUP_TOKEN_FILE=" in _value and "PULSE_SETUP_TOKEN=" not in _value and setup_token not in _value else: - valid = ("PULSE_SETUP_TOKEN=" in _value and setup_token in _value) if _requires_token else ("PULSE_SETUP_TOKEN=" not in _value and setup_token not in _value) + valid = "PULSE_SETUP_TOKEN_FILE=" not in _value and (("PULSE_SETUP_TOKEN=" in _value and setup_token in _value) if _requires_token else ("PULSE_SETUP_TOKEN=" not in _value and setup_token not in _value)) if not valid: if _field_name == "command": setup_command = "" diff --git a/internal/api/configapi/config_handlers_cluster_additional_test.go b/internal/api/configapi/config_handlers_cluster_additional_test.go index 3bf2ab49b1..ab0d4cf5bb 100644 --- a/internal/api/configapi/config_handlers_cluster_additional_test.go +++ b/internal/api/configapi/config_handlers_cluster_additional_test.go @@ -542,10 +542,10 @@ func TestHandleAgentInstallCommand(t *testing.T) { if resp.Token == "" || resp.Command == "" { t.Fatalf("expected token and command in response") } - if !bytes.Contains([]byte(resp.Command), []byte(resp.Token)) { - t.Fatalf("expected command to include token") + if bytes.Contains([]byte(resp.Command), []byte(resp.Token)) || !strings.Contains(resp.Command, `--token-file "$token_file"`) { + t.Fatal("credential must be entered privately, not included in copied command") } - if !bytes.Contains([]byte(resp.Command), []byte("--proxmox-type "+posixShellQuote("pve"))) { + if resp.Command != BuildProxmoxAgentInstallCommand(AgentInstallCommandOptions{BaseURL: "http://example.com:8080", Token: resp.Token, InstallType: "pve", IncludeInstallType: true, EnableCommands: true}) { t.Fatalf("expected command to include proxmox type") } if len(cfg.APITokens) != 1 { @@ -633,7 +633,7 @@ func TestHandleAgentInstallCommand_NormalizesType(t *testing.T) { if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to decode response: %v", err) } - if !bytes.Contains([]byte(resp.Command), []byte("--proxmox-type "+posixShellQuote("pbs"))) { + if resp.Command != BuildProxmoxAgentInstallCommand(AgentInstallCommandOptions{BaseURL: "http://example.com:8080", Token: resp.Token, InstallType: "pbs", IncludeInstallType: true}) { t.Fatalf("expected normalized proxmox type in command, got: %s", resp.Command) } } diff --git a/internal/api/configapi/config_handlers_setup_script_test.go b/internal/api/configapi/config_handlers_setup_script_test.go index 16a06dd083..53bd2a12de 100644 --- a/internal/api/configapi/config_handlers_setup_script_test.go +++ b/internal/api/configapi/config_handlers_setup_script_test.go @@ -435,7 +435,7 @@ func TestPVESetupScript_UsesFailFastRetryGuidance(t *testing.T) { } script := rr.Body.String() - if !containsString(script, `PULSE_BOOTSTRAP_COMMAND_WITH_ENV='curl -fsSL '"'"'http://sentinel-url:7656/api/setup-script?host=http%3A%2F%2Fsentinel-host%3A8006&pulse_url=http%3A%2F%2Fsentinel-url%3A7656&type=pve'"'"' | `) { + if !containsString(script, `PULSE_BOOTSTRAP_COMMAND_WITH_ENV=`+posixShellQuote(BuildSetupScriptCommand("http://sentinel-url:7656/api/setup-script?host=http%3A%2F%2Fsentinel-host%3A8006&pulse_url=http%3A%2F%2Fsentinel-url%3A7656&type=pve", ""))) { t.Fatalf("expected canonical bootstrap command owner in setup script, got: %s", truncate(script, 700)) } if !containsString(script, `echo " $PULSE_BOOTSTRAP_COMMAND_WITH_ENV"`) { @@ -952,7 +952,8 @@ func TestPVESetupScriptSmokeTestsCreatedTokenBeforeAutoRegistration(t *testing.T for _, needle := range []string{ `smoke_test_pve_token() {`, `curl -kfsS --retry 2 --retry-delay 1`, - `Authorization: PVEAPIToken=$PULSE_TOKEN_ID=$TOKEN_VALUE`, + `printf 'Authorization: PVEAPIToken=%s=%s\n' "$PULSE_TOKEN_ID" "$TOKEN_VALUE" | curl`, + `-H @-`, `${HOST_URL%/}/api2/json/nodes`, `TOKEN_READY=false`, `if smoke_test_pve_token; then`, diff --git a/internal/api/configapi/hosted_agent_install_token_test.go b/internal/api/configapi/hosted_agent_install_token_test.go index b1b1c4bb73..a8d2f8dab4 100644 --- a/internal/api/configapi/hosted_agent_install_token_test.go +++ b/internal/api/configapi/hosted_agent_install_token_test.go @@ -51,8 +51,12 @@ func TestHostedRuntimeMintsAgentInstallTokensWithoutLocalAuth(t *testing.T) { if len(cfg.APITokens) != 1 { t.Fatalf("persisted API tokens = %d, want 1", len(cfg.APITokens)) } - if resp.Command != "" && !strings.Contains(resp.Command, resp.Token) { - t.Fatalf("install command does not carry the minted token: %s", resp.Command) + if tc.name == "proxmox agent" { + if strings.Contains(resp.Command, resp.Token) || !strings.Contains(resp.Command, `--token-file "$token_file"`) { + t.Fatal("hosted Proxmox credential must use separate private input") + } + } else if resp.Command != "" && !strings.Contains(resp.Command, resp.Token) { + t.Fatal("generic host command lost its existing credential transport") } }) } diff --git a/internal/api/configapi/install_command_test.go b/internal/api/configapi/install_command_test.go index 605c9d0a0e..b35d089dd9 100644 --- a/internal/api/configapi/install_command_test.go +++ b/internal/api/configapi/install_command_test.go @@ -372,3 +372,25 @@ func TestPrivateBootstrapInteractiveHistoryContainsCommandButNotToken(t *testing t.Fatal("silent input entered shell history or terminal output") } } + +// The public server installer is not the telemetry installer. If it is served +// accidentally at the agent path, its real parser rejects --url before token +// input or privileged installation; a complete but wrong fetch is not success. +func TestPrivateAgentBootstrapRejectsActualServerInstaller(t *testing.T) { + source, err := os.ReadFile(filepath.Join("..", "..", "..", "install.sh")) + if err != nil { + t.Fatal(err) + } + root, env := bootstrapFixture(t, string(source)) + command := BuildProxmoxAgentInstallCommand(AgentInstallCommandOptions{BaseURL: "https://pulse.example", Token: "synthetic-token", InstallType: "pve", IncludeInstallType: true}) + out, err := runBootstrap(t, command, "", env, false) + if err == nil || !strings.Contains(string(out), "Unknown option: --url") { + t.Fatal("wrong installer was not rejected by its real parser") + } + dirs, _ := os.ReadFile(filepath.Join(root, "private-directories")) + for _, path := range strings.Fields(string(dirs)) { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatal("wrong-installer failure left a private directory") + } + } +} diff --git a/scripts/installtests/root_install_sh_test.go b/scripts/installtests/root_install_sh_test.go index 39696cf6a7..a19044b490 100644 --- a/scripts/installtests/root_install_sh_test.go +++ b/scripts/installtests/root_install_sh_test.go @@ -598,7 +598,7 @@ func TestRootInstallScriptAutoRegisterUsesSecureContractShape(t *testing.T) { `'if [ "$(id -u)" -eq 0 ]; then' not in _value`, `'elif command -v sudo >/dev/null 2>&1; then' not in _value`, `valid = "PULSE_SETUP_TOKEN_FILE=" in _value and "PULSE_SETUP_TOKEN=" not in _value and setup_token not in _value`, - `valid = ("PULSE_SETUP_TOKEN=" in _value and setup_token in _value) if _requires_token else ("PULSE_SETUP_TOKEN=" not in _value and setup_token not in _value)`, + `valid = "PULSE_SETUP_TOKEN_FILE=" not in _value and (("PULSE_SETUP_TOKEN=" in _value and setup_token in _value) if _requires_token else ("PULSE_SETUP_TOKEN=" not in _value and setup_token not in _value))`, `not token_hint or token_hint == setup_token`, `[[ "$setup_type" != "pve" ]]`, `[[ "$setup_host" != "$normalized_host_url" ]]`,