From 2420c2affb4b916613ea3c0141ac3bd3ac4f6840 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 24 Dec 2025 17:36:44 +0000 Subject: [PATCH] feat: Commands disabled by default, require --enable-commands to opt-in BREAKING CHANGE: AI command execution on agents is now disabled by default. Users who want AI auto-fix must explicitly enable it with --enable-commands flag or PULSE_ENABLE_COMMANDS=true environment variable. Changes: - Add --enable-commands flag (opt-in for command execution) - Commands disabled by default for security (defense-in-depth) - --disable-commands is now deprecated (logs warning, no longer needed) - PULSE_DISABLE_COMMANDS deprecated in favor of PULSE_ENABLE_COMMANDS - Update installer script to use --enable-commands - Backwards compatibility: PULSE_DISABLE_COMMANDS=false still enables commands This addresses community feedback about secure defaults for arbitrary command execution on production infrastructure. Related to #889 --- cmd/pulse-agent/main.go | 45 ++++++++++++++++++++--- internal/hostagent/agent.go | 9 +++-- internal/hostagent/command_client_test.go | 16 ++++---- scripts/install.sh | 14 +++---- 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/cmd/pulse-agent/main.go b/cmd/pulse-agent/main.go index 47efa5392..b84d971ba 100644 --- a/cmd/pulse-agent/main.go +++ b/cmd/pulse-agent/main.go @@ -134,7 +134,7 @@ func main() { Logger: &logger, EnableProxmox: cfg.EnableProxmox, ProxmoxType: cfg.ProxmoxType, - DisableCommands: cfg.DisableCommands, + EnableCommands: cfg.EnableCommands, } agent, err := hostagent.New(hostCfg) @@ -349,7 +349,7 @@ type Config struct { DisableAutoUpdate bool // Security - DisableCommands bool // Disable command execution for AI auto-fix + EnableCommands bool // Enable command execution for AI auto-fix (disabled by default) // Health/metrics server HealthAddr string @@ -380,7 +380,8 @@ func loadConfig() Config { envEnableProxmox := utils.GetenvTrim("PULSE_ENABLE_PROXMOX") envProxmoxType := utils.GetenvTrim("PULSE_PROXMOX_TYPE") envDisableAutoUpdate := utils.GetenvTrim("PULSE_DISABLE_AUTO_UPDATE") - envDisableCommands := utils.GetenvTrim("PULSE_DISABLE_COMMANDS") + envEnableCommands := utils.GetenvTrim("PULSE_ENABLE_COMMANDS") + envDisableCommands := utils.GetenvTrim("PULSE_DISABLE_COMMANDS") // deprecated envHealthAddr := utils.GetenvTrim("PULSE_HEALTH_ADDR") envKubeconfig := utils.GetenvTrim("PULSE_KUBECONFIG") envKubeContext := utils.GetenvTrim("PULSE_KUBE_CONTEXT") @@ -438,7 +439,8 @@ func loadConfig() Config { enableProxmoxFlag := flag.Bool("enable-proxmox", defaultEnableProxmox, "Enable Proxmox mode (creates API token, registers node)") proxmoxTypeFlag := flag.String("proxmox-type", envProxmoxType, "Proxmox type: pve or pbs (auto-detected if not specified)") disableAutoUpdateFlag := flag.Bool("disable-auto-update", utils.ParseBool(envDisableAutoUpdate), "Disable automatic updates") - disableCommandsFlag := flag.Bool("disable-commands", utils.ParseBool(envDisableCommands), "Disable command execution for AI auto-fix") + enableCommandsFlag := flag.Bool("enable-commands", utils.ParseBool(envEnableCommands), "Enable command execution for AI auto-fix (disabled by default)") + disableCommandsFlag := flag.Bool("disable-commands", false, "[DEPRECATED] Commands are now disabled by default; use --enable-commands to enable") healthAddrFlag := flag.String("health-addr", defaultHealthAddr, "Health/metrics server address (empty to disable)") kubeconfigFlag := flag.String("kubeconfig", envKubeconfig, "Path to kubeconfig (optional; uses in-cluster config if available)") kubeContextFlag := flag.String("kube-context", envKubeContext, "Kubeconfig context (optional)") @@ -508,7 +510,7 @@ func loadConfig() Config { EnableProxmox: *enableProxmoxFlag, ProxmoxType: strings.TrimSpace(*proxmoxTypeFlag), DisableAutoUpdate: *disableAutoUpdateFlag, - DisableCommands: *disableCommandsFlag, + EnableCommands: resolveEnableCommands(*enableCommandsFlag, *disableCommandsFlag, envEnableCommands, envDisableCommands), HealthAddr: strings.TrimSpace(*healthAddrFlag), KubeconfigPath: strings.TrimSpace(*kubeconfigFlag), KubeContext: strings.TrimSpace(*kubeContextFlag), @@ -585,6 +587,39 @@ func defaultLogLevel(envValue string) string { return envValue } +// resolveEnableCommands determines whether command execution should be enabled. +// Priority: --enable-commands > --disable-commands (deprecated) > PULSE_ENABLE_COMMANDS > PULSE_DISABLE_COMMANDS (deprecated) +// Default: disabled (false) for security +func resolveEnableCommands(enableFlag, disableFlag bool, envEnable, envDisable string) bool { + // If --enable-commands is explicitly set, use it + if enableFlag { + return true + } + + // Backwards compat: if --disable-commands was used, log deprecation but respect it + // (disableFlag being true means commands should be disabled, which is already the default) + if disableFlag { + fmt.Fprintln(os.Stderr, "warning: --disable-commands is deprecated and no longer needed (commands are disabled by default). Use --enable-commands to enable.") + return false + } + + // Check environment variables + if envEnable != "" { + return utils.ParseBool(envEnable) + } + + // Backwards compat: PULSE_DISABLE_COMMANDS=true means commands disabled (already default) + // PULSE_DISABLE_COMMANDS=false means commands enabled (backwards compat) + if envDisable != "" { + fmt.Fprintln(os.Stderr, "warning: PULSE_DISABLE_COMMANDS is deprecated. Use PULSE_ENABLE_COMMANDS=true to enable commands.") + // Invert: DISABLE=false means enable + return !utils.ParseBool(envDisable) + } + + // Default: commands disabled + return false +} + // initDockerWithRetry attempts to initialize the Docker agent with exponential backoff. // It returns the agent when Docker becomes available, or nil if the context is cancelled. // Retry intervals: 5s, 10s, 20s, 40s, 80s, 160s, then cap at 5 minutes. diff --git a/internal/hostagent/agent.go b/internal/hostagent/agent.go index 5c4e40c2e..afe0c2b8b 100644 --- a/internal/hostagent/agent.go +++ b/internal/hostagent/agent.go @@ -46,7 +46,7 @@ type Config struct { ProxmoxType string // "pve", "pbs", or "" for auto-detect // Security options - DisableCommands bool // If true, disables the command execution feature (AI auto-fix) + EnableCommands bool // If true, enables the command execution feature (AI auto-fix) } // Agent is responsible for collecting host metrics and shipping them to Pulse. @@ -227,11 +227,12 @@ func New(cfg Config) (*Agent, error) { reportBuffer: buffer.New[agentshost.Report](bufferCapacity), } - // Create command client for AI command execution (unless disabled) - if !cfg.DisableCommands { + // Create command client for AI command execution (only if enabled) + if cfg.EnableCommands { agent.commandClient = NewCommandClient(cfg, agentID, hostname, platform, agentVersion) + cfg.Logger.Info().Msg("Command execution enabled via --enable-commands flag") } else { - cfg.Logger.Info().Msg("Command execution disabled via --disable-commands flag") + cfg.Logger.Info().Msg("Command execution disabled (use --enable-commands to enable)") } return agent, nil diff --git a/internal/hostagent/command_client_test.go b/internal/hostagent/command_client_test.go index a78bb1174..642cde2b2 100644 --- a/internal/hostagent/command_client_test.go +++ b/internal/hostagent/command_client_test.go @@ -11,9 +11,10 @@ func TestNew_DefaultPulseURLUsedForCommandClient(t *testing.T) { logger := zerolog.New(io.Discard) agent, err := New(Config{ - APIToken: "test-token", - LogLevel: zerolog.InfoLevel, - Logger: &logger, + APIToken: "test-token", + LogLevel: zerolog.InfoLevel, + Logger: &logger, + EnableCommands: true, // Commands are disabled by default; enable for this test }) if err != nil { t.Fatalf("New: %v", err) @@ -38,10 +39,11 @@ func TestNew_TrimsPulseURLForCommandClient(t *testing.T) { logger := zerolog.New(io.Discard) agent, err := New(Config{ - PulseURL: "https://example.invalid/", - APIToken: "test-token", - LogLevel: zerolog.InfoLevel, - Logger: &logger, + PulseURL: "https://example.invalid/", + APIToken: "test-token", + LogLevel: zerolog.InfoLevel, + Logger: &logger, + EnableCommands: true, // Commands are disabled by default; enable for this test }) if err != nil { t.Fatalf("New: %v", err) diff --git a/scripts/install.sh b/scripts/install.sh index 582010144..266fd5a36 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -17,7 +17,7 @@ # --interval Reporting interval (default: 30s) # --agent-id Custom agent identifier (default: auto-generated) # --insecure Skip TLS certificate verification -# --disable-commands Disable AI command execution on agent +# --enable-commands Enable AI command execution on agent (disabled by default) # --uninstall Remove the agent # # Auto-Detection: @@ -73,7 +73,7 @@ PROXMOX_TYPE="" UNINSTALL="false" INSECURE="false" AGENT_ID="" -DISABLE_COMMANDS="false" +ENABLE_COMMANDS="false" # Track if flags were explicitly set (to override auto-detection) DOCKER_EXPLICIT="false" @@ -170,7 +170,7 @@ build_exec_args() { if [[ "$ENABLE_PROXMOX" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-proxmox"; fi if [[ -n "$PROXMOX_TYPE" ]]; then EXEC_ARGS="$EXEC_ARGS --proxmox-type ${PROXMOX_TYPE}"; fi if [[ "$INSECURE" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --insecure"; fi - if [[ "$DISABLE_COMMANDS" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --disable-commands"; fi + if [[ "$ENABLE_COMMANDS" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-commands"; fi if [[ -n "$AGENT_ID" ]]; then EXEC_ARGS="$EXEC_ARGS --agent-id ${AGENT_ID}"; fi } @@ -189,7 +189,7 @@ build_exec_args_array() { if [[ "$ENABLE_PROXMOX" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-proxmox); fi if [[ -n "$PROXMOX_TYPE" ]]; then EXEC_ARGS_ARRAY+=(--proxmox-type "$PROXMOX_TYPE"); fi if [[ "$INSECURE" == "true" ]]; then EXEC_ARGS_ARRAY+=(--insecure); fi - if [[ "$DISABLE_COMMANDS" == "true" ]]; then EXEC_ARGS_ARRAY+=(--disable-commands); fi + if [[ "$ENABLE_COMMANDS" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-commands); fi if [[ -n "$AGENT_ID" ]]; then EXEC_ARGS_ARRAY+=(--agent-id "$AGENT_ID"); fi } @@ -209,7 +209,7 @@ while [[ $# -gt 0 ]]; do --disable-proxmox) ENABLE_PROXMOX="false"; PROXMOX_EXPLICIT="true"; shift ;; --proxmox-type) PROXMOX_TYPE="$2"; shift 2 ;; --insecure) INSECURE="true"; shift ;; - --disable-commands) DISABLE_COMMANDS="true"; shift ;; + --enable-commands) ENABLE_COMMANDS="true"; shift ;; --uninstall) UNINSTALL="true"; shift ;; --agent-id) AGENT_ID="$2"; shift 2 ;; *) fail "Unknown argument: $1" ;; @@ -647,9 +647,9 @@ if [[ "$OS" == "darwin" ]]; then PLIST_ARGS="${PLIST_ARGS} --insecure" fi - if [[ "$DISABLE_COMMANDS" == "true" ]]; then + if [[ "$ENABLE_COMMANDS" == "true" ]]; then PLIST_ARGS="${PLIST_ARGS} - --disable-commands" + --enable-commands" fi if [[ -n "$AGENT_ID" ]]; then PLIST_ARGS="${PLIST_ARGS}