diff --git a/internal/fail2ban/connector_agent.go b/internal/fail2ban/connector_agent.go index 6f4b609..87522d9 100644 --- a/internal/fail2ban/connector_agent.go +++ b/internal/fail2ban/connector_agent.go @@ -410,6 +410,21 @@ func (ac *AgentConnector) TestLogpathWithResolution(ctx context.Context, logpath // UpdateDefaultSettings implements Connector. func (ac *AgentConnector) UpdateDefaultSettings(ctx context.Context, settings config.AppSettings) error { + // Check jail.local integrity first + exists, hasUI, chkErr := ac.CheckJailLocalIntegrity(ctx) + if chkErr != nil { + config.DebugLog("Warning: could not check jail.local integrity on agent %s: %v", ac.server.Name, chkErr) + } + if exists && !hasUI { + return fmt.Errorf("jail.local on agent server %s is not managed by Fail2ban-UI - skipping settings update (please migrate your jail.local manually)", ac.server.Name) + } + if !exists { + config.DebugLog("jail.local does not exist on agent server %s - initializing fresh managed file", ac.server.Name) + if err := ac.EnsureJailLocalStructure(ctx); err != nil { + return fmt.Errorf("failed to initialize jail.local on agent server %s: %w", ac.server.Name, err) + } + } + // Convert IgnoreIPs array to space-separated string ignoreIPStr := strings.Join(settings.IgnoreIPs, " ") if ignoreIPStr == "" { diff --git a/internal/fail2ban/connector_ssh.go b/internal/fail2ban/connector_ssh.go index 01114ed..083d74e 100644 --- a/internal/fail2ban/connector_ssh.go +++ b/internal/fail2ban/connector_ssh.go @@ -1555,10 +1555,28 @@ PYEOF func (sc *SSHConnector) UpdateDefaultSettings(ctx context.Context, settings config.AppSettings) error { jailLocalPath := "/etc/fail2ban/jail.local" - // Read existing file if it exists + // Check jail.local integrity first + exists, hasUI, chkErr := sc.CheckJailLocalIntegrity(ctx) + if chkErr != nil { + config.DebugLog("Warning: could not check jail.local integrity on %s: %v", sc.server.Name, chkErr) + } + + if exists && !hasUI { + // File belongs to the user – never overwrite + return fmt.Errorf("jail.local on server %s is not managed by Fail2ban-UI - skipping settings update (please migrate your jail.local manually)", sc.server.Name) + } + + if !exists { + // File was deleted (e.g. user finished migration) – create a fresh managed file + config.DebugLog("jail.local does not exist on server %s - initializing fresh managed file", sc.server.Name) + if err := sc.EnsureJailLocalStructure(ctx); err != nil { + return fmt.Errorf("failed to initialize jail.local on server %s: %w", sc.server.Name, err) + } + } + + // Read existing file existingContent, err := sc.runRemoteCommand(ctx, []string{"cat", jailLocalPath}) if err != nil { - // File doesn't exist, create new one existingContent = "" } diff --git a/internal/fail2ban/jail_management.go b/internal/fail2ban/jail_management.go index c760fbb..3a18645 100644 --- a/internal/fail2ban/jail_management.go +++ b/internal/fail2ban/jail_management.go @@ -1071,14 +1071,35 @@ func UpdateDefaultSettingsLocal(settings config.AppSettings) error { config.DebugLog("UpdateDefaultSettingsLocal called") localPath := "/etc/fail2ban/jail.local" - // Read existing file if it exists + // Check jail.local integrity first var existingContent string + fileExists := false if content, err := os.ReadFile(localPath); err == nil { existingContent = string(content) + fileExists = len(strings.TrimSpace(existingContent)) > 0 } else if !os.IsNotExist(err) { return fmt.Errorf("failed to read jail.local: %w", err) } + hasUIAction := strings.Contains(existingContent, "ui-custom-action") + + if fileExists && !hasUIAction { + // File belongs to the user – never overwrite + return fmt.Errorf("jail.local is not managed by Fail2ban-UI - skipping settings update (please migrate your jail.local manually)") + } + + if !fileExists { + // File was deleted (e.g. user finished migration); create a fresh managed file + config.DebugLog("jail.local does not exist - initializing fresh managed file") + if err := config.EnsureJailLocalStructure(); err != nil { + return fmt.Errorf("failed to initialize jail.local: %w", err) + } + // Re-read the freshly created file + if content, err := os.ReadFile(localPath); err == nil { + existingContent = string(content) + } + } + // Remove commented lines (lines starting with #) but preserve: // - Banner lines (containing "Fail2Ban-UI" or "fail2ban-ui") // - action_mwlg and action override lines diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index ba5b274..9f4dcf6 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -154,8 +154,17 @@ func SummaryHandler(c *gin.Context) { // Check jail.local integrity on every summary request so the dashboard // can display a persistent warning banner when the file is not managed by us. - if exists, hasUI, chkErr := conn.CheckJailLocalIntegrity(c.Request.Context()); chkErr == nil && exists && !hasUI { - resp.JailLocalWarning = true + if exists, hasUI, chkErr := conn.CheckJailLocalIntegrity(c.Request.Context()); chkErr == nil { + if exists && !hasUI { + resp.JailLocalWarning = true + } else if !exists { + // File was removed (user finished migration) – initialize a fresh managed file + if err := conn.EnsureJailLocalStructure(c.Request.Context()); err != nil { + config.DebugLog("Warning: failed to initialize jail.local on summary request: %v", err) + } else { + config.DebugLog("Initialized fresh jail.local for server %s (file was missing)", conn.Server().Name) + } + } } c.JSON(http.StatusOK, resp) @@ -780,11 +789,18 @@ func TestServerHandler(c *gin.Context) { return } - // Check jail.local integrity: if it exists but is not managed by fail2ban-ui, warn the user + // Check jail.local integrity: if it exists but is not managed by fail2ban-ui, warn the user. + // If the file was removed (user finished migration), initialize a fresh managed file. resp := gin.H{"messageKey": "servers.actions.test_success"} if exists, hasUI, err := conn.CheckJailLocalIntegrity(ctx); err == nil { if exists && !hasUI { resp["jailLocalWarning"] = true + } else if !exists { + if err := conn.EnsureJailLocalStructure(ctx); err != nil { + config.DebugLog("Warning: failed to initialize jail.local on test request: %v", err) + } else { + config.DebugLog("Initialized fresh jail.local for server %s (file was missing)", conn.Server().Name) + } } } c.JSON(http.StatusOK, resp) @@ -2172,7 +2188,7 @@ func UpdateSettingsHandler(c *gin.Context) { config.DebugLog("Updating DEFAULT settings on server: %s (type: %s)", server.Name, server.Type) if err := conn.UpdateDefaultSettings(c.Request.Context(), newSettings); err != nil { errorMsg := fmt.Sprintf("Failed to update DEFAULT settings on %s: %v", server.Name, err) - config.DebugLog("Error: %s", errorMsg) + log.Printf("⚠️ %s", errorMsg) errors = append(errors, errorMsg) } else { config.DebugLog("Successfully updated DEFAULT settings on %s", server.Name)