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
This commit is contained in:
rcourtman
2025-12-24 17:36:44 +00:00
parent 73a92813f5
commit 2420c2affb
4 changed files with 61 additions and 23 deletions
+40 -5
View File
@@ -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.
+5 -4
View File
@@ -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
+9 -7
View File
@@ -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)
+7 -7
View File
@@ -17,7 +17,7 @@
# --interval <dur> Reporting interval (default: 30s)
# --agent-id <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}
<string>--insecure</string>"
fi
if [[ "$DISABLE_COMMANDS" == "true" ]]; then
if [[ "$ENABLE_COMMANDS" == "true" ]]; then
PLIST_ARGS="${PLIST_ARGS}
<string>--disable-commands</string>"
<string>--enable-commands</string>"
fi
if [[ -n "$AGENT_ID" ]]; then
PLIST_ARGS="${PLIST_ARGS}