mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
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:
+40
-5
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user