mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 03:08:32 +00:00
Allow insecure TLS for Proxmox agent installs
This commit is contained in:
parent
70469660b3
commit
0ac02e43c5
14 changed files with 147 additions and 23 deletions
|
|
@ -172,6 +172,11 @@ Docker/Podman module attached to that host identity. The removal must delete
|
|||
the associated runtime report surfaces and clear their active alerts instead
|
||||
of leaving container alerts or inventory detached from a removed agent.
|
||||
|
||||
Proxmox node and backup-server setup may request an explicitly insecure
|
||||
Unified Agent installer command. The choice must remain off by default, be
|
||||
visible at the point where the command is generated, and affect both the
|
||||
installer download and the agent's subsequent Pulse TLS connection.
|
||||
|
||||
## Canonical Files
|
||||
|
||||
1. `internal/api/agent_install_command_shared.go`
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@ no infrastructure name, target, address, error text, or arbitrary alert
|
|||
metadata. A host-offline alert is eligible only when that host currently owns
|
||||
an enabled external-probe assignment.
|
||||
|
||||
`POST /api/agent-install-command` accepts an optional `insecure` boolean for
|
||||
Proxmox installer generation. When true, the returned command uses insecure
|
||||
TLS for the installer download and passes `--insecure` to the agent; omission
|
||||
or false preserves certificate verification. The server, not the browser,
|
||||
continues to mint and quote the enrollment token.
|
||||
|
||||
## Canonical Files
|
||||
|
||||
1a. `docs/API.md`
|
||||
|
|
|
|||
|
|
@ -99,6 +99,11 @@ retaining agent check time as observation metadata; storage/recovery must not
|
|||
reinterpret either timestamp as backup recency, datastore health, or recovery
|
||||
freshness.
|
||||
|
||||
Agent-install command generation under `internal/api/` may carry an explicit
|
||||
TLS-verification override, but it does not alter storage credentials, backup
|
||||
authority, restore behavior, or persisted recovery state. The generated
|
||||
command remains a bounded onboarding artifact with server-owned token quoting.
|
||||
|
||||
## Canonical Files
|
||||
|
||||
1. `internal/recovery/index.go`
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
{
|
||||
"version": 1,
|
||||
"base_sha": "3826316eec488cef2561eb64397f225979a0adc1",
|
||||
"verified_at": "2026-08-11T14:57:15Z",
|
||||
"base_sha": "70469660b3e38895ff28b50d28e320e975a3e908",
|
||||
"verified_at": "2026-08-11T14:08:23Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/WhatsNewCard.tsx",
|
||||
"frontend-modern/src/components/whatsNewModel.ts"
|
||||
"frontend-modern/src/api/nodes.ts",
|
||||
"frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx",
|
||||
"frontend-modern/src/components/Settings/useNodeModalState.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/WhatsNewCard.tsx": "f7f905d0e08b56c98b908653ec663332dc9d140d4e82b6d20c9428be6bbd5b15",
|
||||
"frontend-modern/src/components/whatsNewModel.ts": "d6aaf56335e9cb1505e5f574c51f2d396636db850f2162fbc56cf61330df5cc5"
|
||||
"frontend-modern/src/api/nodes.ts": "1f06a674b715cfeef5cfe219e239b54df70e95a09eb2907bc31de8c01dce0012",
|
||||
"frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx": "b21eebe819132f039874415c83c99fee1337b34ebf06e73e91aaacc3181fad96",
|
||||
"frontend-modern/src/components/Settings/useNodeModalState.ts": "973d4ed2fbe5cef53f4e7853888a351b137ca1bcfa7574ff964c4efa83548eaa"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/overview"
|
||||
"/settings/infrastructure"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
|
|
@ -25,18 +27,17 @@
|
|||
}
|
||||
],
|
||||
"states": [
|
||||
"Post-update Pulse v6.2.2 changelog open with Added, Improved, and Fixed sections while Highlights and Release Qualification are absent.",
|
||||
"Desktop dialog fully visible with all categorized entries fitting without page clipping or horizontal overflow.",
|
||||
"Mobile dialog open at the top with internal vertical scrolling and no horizontal overflow.",
|
||||
"Mobile dialog scrolled to the final Fixed entry with the close action still reachable.",
|
||||
"Dialog dismissed with Escape, reopened, and dismissed by clicking the backdrop."
|
||||
"Proxmox VE source dialog on the API inventory strategy with the TLS override absent.",
|
||||
"Proxmox VE source dialog on Host Telemetry Agent with the TLS override visible, off by default, and then checked.",
|
||||
"Desktop dialog at 1440 by 1000 with the full control label visible, focused, and no page or modal horizontal overflow.",
|
||||
"Narrow dialog at 390 by 844 with the checked control and wrapped label reachable in the modal scroll region, no page or modal horizontal overflow, and the surrounding SSL settings unclipped.",
|
||||
"Reopened Proxmox host-agent dialog after dismissal with the TLS override reset to off."
|
||||
],
|
||||
"interactions": [
|
||||
"Triggered the post-update dialog from a simulated completed v6.2.2 update and inspected the rendered changelog content.",
|
||||
"Resized from 1440 by 900 pixels to 390 by 844 pixels and confirmed responsive containment.",
|
||||
"Scrolled the changelog body to the bottom and confirmed the final entry remained readable.",
|
||||
"Dismissed with Escape and confirmed page scrolling was restored.",
|
||||
"Reopened the dialog and dismissed it by clicking outside the panel.",
|
||||
"Inspected browser warning and error logs and found none."
|
||||
"Opened Add infrastructure, chose Proxmox VE, and switched from the recommended API inventory path to Host Telemetry Agent.",
|
||||
"Clicked the TLS override and confirmed the native checkbox changed from unchecked to checked while retaining focus.",
|
||||
"Repeated the host-agent surface inspection at 390 by 844 pixels and scrolled the dialog to the TLS and SSL controls.",
|
||||
"Dismissed the dialog with Escape, confirmed focus returned to Add infrastructure, reopened the flow, and confirmed the override did not persist.",
|
||||
"Closed the reopened dialog with its named close button."
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,13 +298,19 @@ describe('NodesAPI', () => {
|
|||
type: 'pve',
|
||||
enableProxmox: true,
|
||||
enableCommands: true,
|
||||
insecure: true,
|
||||
});
|
||||
|
||||
expect(apiFetchJSON).toHaveBeenCalledWith(
|
||||
'/api/agent-install-command',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type: 'pve', enableProxmox: true, enableCommands: true }),
|
||||
body: JSON.stringify({
|
||||
type: 'pve',
|
||||
enableProxmox: true,
|
||||
enableCommands: true,
|
||||
insecure: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({ command: 'curl ...' });
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ export class NodesAPI {
|
|||
type: 'pve' | 'pbs';
|
||||
enableProxmox: boolean;
|
||||
enableCommands?: boolean;
|
||||
insecure?: boolean;
|
||||
}): Promise<AgentInstallCommandResponse> {
|
||||
const response = await apiFetchJSON<RawAgentInstallCommandResponse>(
|
||||
'/api/agent-install-command',
|
||||
|
|
|
|||
|
|
@ -223,6 +223,20 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
|
|||
<Show when={state.agentCommandError()}>
|
||||
<p class="text-xs text-red-500">{state.agentCommandError()}</p>
|
||||
</Show>
|
||||
<label class="flex items-start gap-2 text-xs text-base-content">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state.agentInstallInsecure()}
|
||||
onChange={(event) =>
|
||||
state.setAgentInstallInsecure(event.currentTarget.checked)
|
||||
}
|
||||
class="mt-0.5 rounded text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span>
|
||||
Skip TLS certificate verification when downloading the installer and
|
||||
connecting the agent to Pulse
|
||||
</span>
|
||||
</label>
|
||||
<p class="text-[11px] text-muted italic">
|
||||
No token fields are needed here. The node appears in Pulse automatically after
|
||||
the agent starts.
|
||||
|
|
@ -696,6 +710,20 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
|
|||
<Show when={state.agentCommandError()}>
|
||||
<p class="text-xs text-red-500">{state.agentCommandError()}</p>
|
||||
</Show>
|
||||
<label class="flex items-start gap-2 text-xs text-base-content">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state.agentInstallInsecure()}
|
||||
onChange={(event) =>
|
||||
state.setAgentInstallInsecure(event.currentTarget.checked)
|
||||
}
|
||||
class="mt-0.5 rounded text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span>
|
||||
Skip TLS certificate verification when downloading the installer and
|
||||
connecting the agent to Pulse
|
||||
</span>
|
||||
</label>
|
||||
<p class="text-xs text-muted">
|
||||
No token fields are needed here. The server appears in Pulse automatically after
|
||||
the agent connects.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const renderSetupGuide = (
|
|||
const copyQuickSetupCommand = vi.fn();
|
||||
const Harness = () => {
|
||||
const [setupMode, setSetupMode] = createSignal<NodeModalSetupMode>('auto');
|
||||
const [agentInstallInsecure, setAgentInstallInsecure] = createSignal(false);
|
||||
const updateField = vi.fn((field: string, value: string | boolean | number) => {
|
||||
if (field === 'setupMode') {
|
||||
setSetupMode(value as NodeModalSetupMode);
|
||||
|
|
@ -28,6 +29,7 @@ const renderSetupGuide = (
|
|||
const state = {
|
||||
agentCommandError: () => null,
|
||||
agentInstallCommand: () => '',
|
||||
agentInstallInsecure,
|
||||
copyCommand,
|
||||
copyProxmoxAgentInstallCommand: vi.fn(),
|
||||
copyQuickSetupCommand,
|
||||
|
|
@ -40,6 +42,7 @@ const renderSetupGuide = (
|
|||
quickSetupExpiryLabel: () => '',
|
||||
quickSetupCommandReady: () => Boolean(options.quickSetupCommandReady),
|
||||
quickSetupTokenHint: () => options.quickSetupTokenHint ?? '',
|
||||
setAgentInstallInsecure,
|
||||
updateField,
|
||||
} as unknown as NodeModalState;
|
||||
|
||||
|
|
@ -109,6 +112,12 @@ describe('NodeModalSetupGuideSection', () => {
|
|||
expect(screen.getByText(/temperatures, SMART, ZFS, Ceph, and mdadm/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Docker inside Proxmox LXCs:/i)).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/Pulse command execution/i).length).toBeGreaterThan(0);
|
||||
const tlsOverride = screen.getByRole('checkbox', {
|
||||
name: /Skip TLS certificate verification when downloading the installer/i,
|
||||
});
|
||||
expect(tlsOverride).not.toBeChecked();
|
||||
fireEvent.click(tlsOverride);
|
||||
expect(tlsOverride).toBeChecked();
|
||||
expect(
|
||||
screen.getByText('PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true'),
|
||||
).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -112,6 +112,16 @@ const settingsRuntimeSources = import.meta.glob(['../*.tsx', '../ConnectionEdito
|
|||
}) as Record<string, string>;
|
||||
|
||||
describe('settings architecture guardrails', () => {
|
||||
it('keeps the Proxmox installer TLS override explicit and request-bound', () => {
|
||||
expect(nodeModalSetupGuideSectionSource).toContain(
|
||||
'Skip TLS certificate verification when downloading the installer and',
|
||||
);
|
||||
expect(nodeModalSetupGuideSectionSource).toContain('connecting the agent to Pulse');
|
||||
expect(nodeModalSetupGuideSectionSource).toContain('state.agentInstallInsecure()');
|
||||
expect(nodeModalSetupGuideSectionSource).toContain('state.setAgentInstallInsecure');
|
||||
expect(nodeModalStateSource).toContain('insecure: agentInstallInsecure()');
|
||||
});
|
||||
|
||||
it('keeps Unified Agent lifecycle failures on the shared connections contract', () => {
|
||||
expect(connectionsApiSource).toContain("| 'failed'");
|
||||
expect(connectionsApiSource).toContain("'disabled' | 'disconnected' | 'enabled'");
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
|
|||
const [agentInstallCommand, setAgentInstallCommand] = createSignal('');
|
||||
const [loadingAgentCommand, setLoadingAgentCommand] = createSignal(false);
|
||||
const [agentCommandError, setAgentCommandError] = createSignal<string | null>(null);
|
||||
const [agentInstallInsecure, setAgentInstallInsecure] = createSignal(false);
|
||||
|
||||
const isAdvancedSetupMode = () => formData().setupMode === 'manual';
|
||||
const showTemperatureMonitoringSection = () =>
|
||||
|
|
@ -84,6 +85,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
|
|||
type,
|
||||
enableProxmox: true,
|
||||
enableCommands: type === 'pve',
|
||||
insecure: agentInstallInsecure(),
|
||||
});
|
||||
setAgentInstallCommand(data.command);
|
||||
const copied = await copyToClipboard(data.command);
|
||||
|
|
@ -560,6 +562,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
|
|||
return {
|
||||
agentCommandError,
|
||||
agentInstallCommand,
|
||||
agentInstallInsecure,
|
||||
copyCommand,
|
||||
copyProxmoxAgentInstallCommand,
|
||||
copyQuickSetupCommand,
|
||||
|
|
@ -575,6 +578,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
|
|||
quickSetupExpiryLabel,
|
||||
quickSetupCommandReady,
|
||||
quickSetupTokenHint,
|
||||
setAgentInstallInsecure,
|
||||
showTemperatureMonitoringSection,
|
||||
temperatureMonitoringEnabledValue,
|
||||
testResult,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ type agentInstallCommandOptions struct {
|
|||
InstallType string
|
||||
IncludeInstallType bool
|
||||
EnableCommands bool
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
type setupScriptInstallArtifact struct {
|
||||
|
|
@ -187,6 +188,10 @@ func withPrivilegeEscalation(command string) string {
|
|||
func buildProxmoxAgentInstallCommand(opts agentInstallCommandOptions) string {
|
||||
baseURL := normalizeAgentInstallBaseURL(opts.BaseURL)
|
||||
installScriptURL := baseURL + "/install.sh"
|
||||
curlFlags := "-fsSL"
|
||||
if opts.Insecure {
|
||||
curlFlags = "-kfsSL"
|
||||
}
|
||||
token := strings.TrimSpace(opts.Token)
|
||||
tokenSetup := ""
|
||||
tokenArg := ""
|
||||
|
|
@ -197,13 +202,13 @@ func buildProxmoxAgentInstallCommand(opts agentInstallCommandOptions) string {
|
|||
--token-file "$token_file"`
|
||||
tokenCleanup = `; rc=$?; rm -f "$token_file"; exit $rc`
|
||||
}
|
||||
command := fmt.Sprintf(`%scurl -fsSL %s | bash -s -- \
|
||||
command := fmt.Sprintf(`%scurl %s %s | bash -s -- \
|
||||
--url %s \
|
||||
--enable-proxmox`,
|
||||
tokenSetup, posixShellQuote(installScriptURL), posixShellQuote(baseURL))
|
||||
tokenSetup, curlFlags, posixShellQuote(installScriptURL), posixShellQuote(baseURL))
|
||||
command += tokenArg
|
||||
|
||||
if installBaseURLRequiresInsecure(baseURL) {
|
||||
if opts.Insecure || installBaseURLRequiresInsecure(baseURL) {
|
||||
command += ` \
|
||||
--insecure`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,20 @@ func TestBuildProxmoxAgentInstallCommand_IncludesInsecureForPlainHTTP(t *testing
|
|||
require.Contains(t, command, "--insecure")
|
||||
}
|
||||
|
||||
func TestBuildProxmoxAgentInstallCommand_IncludesExplicitInsecureForSelfSignedHTTPS(t *testing.T) {
|
||||
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
|
||||
BaseURL: "https://pulse.example.com:7655/",
|
||||
Token: "token-123",
|
||||
InstallType: "pve",
|
||||
IncludeInstallType: true,
|
||||
Insecure: true,
|
||||
})
|
||||
|
||||
require.Contains(t, command, "curl -kfsSL "+posixShellQuote("https://pulse.example.com:7655/install.sh"))
|
||||
require.Contains(t, command, "--url "+posixShellQuote("https://pulse.example.com:7655"))
|
||||
require.Contains(t, command, "--insecure")
|
||||
}
|
||||
|
||||
func TestBuildProxmoxAgentInstallCommand_UsesPrivilegeEscalationWrapper(t *testing.T) {
|
||||
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
|
||||
BaseURL: "https://pulse.example.com/",
|
||||
|
|
|
|||
|
|
@ -553,6 +553,33 @@ func TestHandleAgentInstallCommand(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleAgentInstallCommand_InsecureHTTPS(t *testing.T) {
|
||||
cfg := &config.Config{DataPath: t.TempDir(), AuthUser: "admin", AuthPass: "hashed-password"}
|
||||
handler := newTestConfigHandlers(t, cfg)
|
||||
|
||||
body := []byte(`{"type":"pve","insecure":true}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/config/agent-install", bytes.NewReader(body))
|
||||
req.Host = "pulse.example.com:7655"
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
rec := httptest.NewRecorder()
|
||||
handler.HandleAgentInstallCommand(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status OK, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp AgentInstallCommandResponse
|
||||
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("curl -kfsSL")) {
|
||||
t.Fatalf("expected insecure installer download in command, got: %s", resp.Command)
|
||||
}
|
||||
if !bytes.Contains([]byte(resp.Command), []byte("--insecure")) {
|
||||
t.Fatalf("expected insecure agent connection in command, got: %s", resp.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAgentInstallCommand_OmitsTokenWhenAuthOptional(t *testing.T) {
|
||||
cfg := &config.Config{DataPath: t.TempDir()}
|
||||
handler := newTestConfigHandlers(t, cfg)
|
||||
|
|
|
|||
|
|
@ -2448,6 +2448,7 @@ func (h *ConfigHandlers) generateOrLoadSSHKey(sshDir, privateKeyPath, publicKeyP
|
|||
type AgentInstallCommandRequest struct {
|
||||
Type string `json:"type"` // "pve", "pbs", or "host"
|
||||
EnableCommands bool `json:"enableCommands,omitempty"`
|
||||
Insecure bool `json:"insecure,omitempty"`
|
||||
Name string `json:"name,omitempty"` // optional token name (host installs)
|
||||
}
|
||||
|
||||
|
|
@ -2526,12 +2527,14 @@ func (h *ConfigHandlers) handleAgentInstallCommand(w http.ResponseWriter, r *htt
|
|||
InstallType: installType,
|
||||
IncludeInstallType: true,
|
||||
EnableCommands: req.EnableCommands,
|
||||
Insecure: req.Insecure,
|
||||
})
|
||||
|
||||
log.Info().
|
||||
Str("type", installType).
|
||||
Bool("token_issued", rawToken != "").
|
||||
Bool("enable_commands", req.EnableCommands).
|
||||
Bool("insecure", req.Insecure).
|
||||
Msg("Generated agent install command")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue