Harden install-time PVE token extraction with JSON-first parsing (#44/#1312)

The install-time auto-register path (auto_register_pve_node) parsed the API
token secret out of pveum's box-drawing table output with a fragile awk
column-split. The web-setup render path was already hardened to request
'pveum ... --output-format json' first and parse the value field, but this
secondary install.sh path was never ported.

auto_register_pve_node now requests --output-format json first (falling back
to the bare --privsep 1 form only when an older pveum rejects the JSON flag,
which keeps the secure-installer contract pin on that form satisfied) and
extracts the secret via a new extract_pve_token_value helper: JSON value-field
parse first, then a locale-independent box-drawing table fallback (normalizes
the column separator to a plain pipe byte-wise before splitting, so it works
regardless of host locale). This mirrors the hardened render path and removes
the silent-failure / mis-parse risk when pveum table formatting drifts.

Functional + contract tests in root_install_sh_test.go; deployment-installability
contract documents the deterministic extraction. The host-agent path
(internal/hostagent/proxmox_setup.go setupPVEToken) carries an agent-lifecycle
token-permission proof obligation and is left for a governed lane.
This commit is contained in:
rcourtman 2026-06-09 15:47:12 +01:00
parent 2ff2b18c74
commit 16f791a656
3 changed files with 132 additions and 3 deletions

View file

@ -723,6 +723,85 @@ func TestRootInstallUninstallWiresSensorProxyCleanup(t *testing.T) {
}
}
// TestRootInstallExtractsPveTokenValue guards the #44/#1312 token-extraction
// hardening for the install-time auto-register path: token capture must prefer
// the deterministic `pveum --output-format json` form and parse the `value`
// field, while still recovering from the legacy box-drawing table layout that
// older pveum builds emit, so it does not silently fail or mis-parse when
// pveum's table formatting drifts.
func TestRootInstallExtractsPveTokenValue(t *testing.T) {
fn := extractRootInstallShellFunction(t, "extract_pve_token_value")
const secret = "12345678-1234-1234-1234-1234567890ab"
jsonOutput := `{"full-tokenid":"pulse-monitor@pve!pulse-x","info":{"privsep":"1"},"value":"` + secret + `"}`
tableOutput := "" +
"┌──────────────┬──────────────────────────────────────┐\n" +
"│ key │ value │\n" +
"╞══════════════╪══════════════════════════════════════╡\n" +
"│ full-tokenid │ pulse-monitor@pve!pulse-x │\n" +
"├──────────────┼──────────────────────────────────────┤\n" +
"│ info │ {\"privsep\":\"1\"} │\n" +
"├──────────────┼──────────────────────────────────────┤\n" +
"│ value │ " + secret + " │\n" +
"└──────────────┴──────────────────────────────────────┘\n"
cases := []struct {
name string
output string
want string
}{
{"json", jsonOutput, secret},
{"table", tableOutput, secret},
{"garbage", "no token here\n", ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
script := fn + "\nextract_pve_token_value \"$TOKEN_OUTPUT\"\n"
cmd := exec.Command("bash", "-c", script)
cmd.Env = append(os.Environ(), "TOKEN_OUTPUT="+tc.output)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("extract_pve_token_value failed: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != tc.want {
t.Fatalf("extract_pve_token_value(%s) = %q, want %q", tc.name, got, tc.want)
}
})
}
}
// TestRootInstallAutoRegisterPrefersJsonTokenForm pins that the install-time
// auto-register path requests the JSON form first and keeps the legacy table
// form only as an explicit fallback (so the secure-installer contract pin on
// the bare form stays satisfied).
func TestRootInstallAutoRegisterPrefersJsonTokenForm(t *testing.T) {
content, err := os.ReadFile(filepath.Join("..", "..", "install.sh"))
if err != nil {
t.Fatalf("read root install.sh: %v", err)
}
script := string(content)
required := []string{
`pveum user token add pulse-monitor@pve "$token_name" --privsep 1 --output-format json 2>&1`,
`pveum user token add pulse-monitor@pve "$token_name" --privsep 1 2>&1`,
`token_value=$(extract_pve_token_value "$token_output"`,
`extract_pve_token_value() {`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing hardened token-extraction contract: %s", needle)
}
}
jsonIdx := strings.Index(script, `--privsep 1 --output-format json 2>&1`)
bareIdx := strings.Index(script, "\n token_output=$(pveum user token add pulse-monitor@pve \"$token_name\" --privsep 1 2>&1)")
if jsonIdx < 0 || bareIdx < 0 || jsonIdx > bareIdx {
t.Fatalf("expected JSON token form to precede the legacy table fallback (json=%d bare=%d)", jsonIdx, bareIdx)
}
}
func TestRootInstallDeployAgentScriptsDeploysSignatureSidecars(t *testing.T) {
extractDir := t.TempDir()
installDir := t.TempDir()