mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Check jail.local state and warn user if it is not fail2ban-UI managed, disable automatic jail.local migration because it is only testing
This commit is contained in:
12
README.md
12
README.md
@@ -322,6 +322,18 @@ On page load, the footer can check the latest release on GitHub to show "Latest"
|
||||
|
||||
When disabled, the footer still shows the current version but does not perform any request to GitHub.
|
||||
|
||||
**Experimental: Automatic jail.local Migration**
|
||||
|
||||
If your Fail2ban server has a pre-existing `jail.local` with custom jails, Fail2ban-UI **will not** migrate them automatically by default. It is always best to migrate a pre-existing `jail.local` by hand (move each jail section into its own file under `jail.d/`).
|
||||
|
||||
To enable the **experimental** automatic migration, set:
|
||||
|
||||
```bash
|
||||
-e JAIL_AUTOMIGRATION=true \
|
||||
```
|
||||
|
||||
> **Warning:** This feature is experimental. A backup of your original `jail.local` should be created before migration, but manual migration is strongly recommended for production systems.
|
||||
|
||||
**OIDC Authentication Configuration (Optional)**
|
||||
|
||||
Enable OIDC authentication by setting the required environment variables. This protects the web UI with your identity provider. The logout flow automatically redirects back to the login page after successful provider logout.
|
||||
|
||||
@@ -876,10 +876,6 @@ func EnsureJailLocalStructure() error {
|
||||
}
|
||||
|
||||
// ensureJailLocalStructure creates or updates jail.local with proper structure:
|
||||
// 1. Banner at top warning users not to edit manually
|
||||
// 2. [DEFAULT] section with current UI settings
|
||||
// 3. action_mwlg configuration
|
||||
// 4. action = %(action_mwlg)s at the end
|
||||
func ensureJailLocalStructure() error {
|
||||
DebugLog("Running ensureJailLocalStructure()") // entry point
|
||||
|
||||
@@ -893,16 +889,25 @@ func ensureJailLocalStructure() error {
|
||||
|
||||
// Read existing jail.local content if it exists
|
||||
var existingContent string
|
||||
fileExists := false
|
||||
if content, err := os.ReadFile(jailFile); err == nil {
|
||||
existingContent = string(content)
|
||||
fileExists = len(strings.TrimSpace(existingContent)) > 0
|
||||
}
|
||||
|
||||
// Check if file already has our full banner (indicating it's already properly structured)
|
||||
// Check for the complete banner pattern with hash line separators
|
||||
// If jail.local exists but is NOT managed by Fail2ban-UI,
|
||||
// it belongs to the user; never overwrite it.
|
||||
hasUIAction := strings.Contains(existingContent, "ui-custom-action")
|
||||
if fileExists && !hasUIAction {
|
||||
DebugLog("jail.local exists but is not managed by Fail2ban-UI - skipping overwrite")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if file already has our full banner; indicating it's already properly structured
|
||||
hasFullBanner := strings.Contains(existingContent, "################################################################################") &&
|
||||
strings.Contains(existingContent, "Fail2Ban-UI Managed Configuration") &&
|
||||
strings.Contains(existingContent, "DO NOT EDIT THIS FILE MANUALLY")
|
||||
hasActionMwlg := strings.Contains(existingContent, "action_mwlg") && strings.Contains(existingContent, "ui-custom-action")
|
||||
hasActionMwlg := strings.Contains(existingContent, "action_mwlg") && hasUIAction
|
||||
hasActionOverride := strings.Contains(existingContent, "action = %(action_mwlg)s")
|
||||
|
||||
// If file is already properly structured, just ensure DEFAULT section is up to date
|
||||
@@ -952,7 +957,7 @@ chain = %s
|
||||
defaultSection += "\n"
|
||||
|
||||
// Build action_mwlg configuration
|
||||
// Note: action_mwlg depends on action_ which depends on banaction (now defined above)
|
||||
// Note: action_mwlg depends on action_ which depends on banaction
|
||||
// The multi-line format uses indentation for continuation
|
||||
// ui-custom-action only needs logpath and chain
|
||||
actionMwlgConfig := `# Custom Fail2Ban action for UI callbacks
|
||||
|
||||
@@ -443,11 +443,31 @@ func (ac *AgentConnector) UpdateDefaultSettings(ctx context.Context, settings co
|
||||
return ac.put(ctx, "/v1/jails/default-settings", payload, nil)
|
||||
}
|
||||
|
||||
// CheckJailLocalIntegrity implements Connector.
|
||||
func (ac *AgentConnector) CheckJailLocalIntegrity(ctx context.Context) (bool, bool, error) {
|
||||
var result struct {
|
||||
Exists bool `json:"exists"`
|
||||
HasUIAction bool `json:"hasUIAction"`
|
||||
}
|
||||
if err := ac.get(ctx, "/v1/jails/check-integrity", &result); err != nil {
|
||||
// If the agent does not implement this endpoint, assume OK
|
||||
if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") {
|
||||
return false, false, nil
|
||||
}
|
||||
return false, false, fmt.Errorf("failed to check jail.local integrity on %s: %w", ac.server.Name, err)
|
||||
}
|
||||
return result.Exists, result.HasUIAction, nil
|
||||
}
|
||||
|
||||
// EnsureJailLocalStructure implements Connector.
|
||||
func (ac *AgentConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
||||
// Call agent API endpoint to ensure jail.local structure
|
||||
// If the endpoint doesn't exist, we'll need to implement it on the agent side
|
||||
// For now, we'll try calling it and handle the error gracefully
|
||||
// Safety: if jail.local exists but is not managed by Fail2ban-UI,
|
||||
// it belongs to the user; never overwrite it.
|
||||
if exists, hasUI, err := ac.CheckJailLocalIntegrity(ctx); err == nil && exists && !hasUI {
|
||||
config.DebugLog("jail.local on agent server %s exists but is not managed by Fail2ban-UI -- skipping overwrite", ac.server.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
return ac.post(ctx, "/v1/jails/ensure-structure", nil, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -343,9 +344,6 @@ func (lc *LocalConnector) UpdateDefaultSettings(ctx context.Context, settings co
|
||||
|
||||
// EnsureJailLocalStructure implements Connector.
|
||||
func (lc *LocalConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
||||
// Note: Migration is handled in newConnectorForServer() before
|
||||
// config.EnsureLocalFail2banAction() is called, so migration has already
|
||||
// run by the time this method is called.
|
||||
return config.EnsureJailLocalStructure()
|
||||
}
|
||||
|
||||
@@ -369,6 +367,20 @@ func (lc *LocalConnector) DeleteFilter(ctx context.Context, filterName string) e
|
||||
return DeleteFilter(filterName)
|
||||
}
|
||||
|
||||
// CheckJailLocalIntegrity implements Connector.
|
||||
func (lc *LocalConnector) CheckJailLocalIntegrity(ctx context.Context) (bool, bool, error) {
|
||||
const jailLocalPath = "/etc/fail2ban/jail.local"
|
||||
content, err := os.ReadFile(jailLocalPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, false, nil // file does not exist; OK, will be created
|
||||
}
|
||||
return false, false, fmt.Errorf("failed to read jail.local: %w", err)
|
||||
}
|
||||
hasUIAction := strings.Contains(string(content), "ui-custom-action")
|
||||
return true, hasUIAction, nil
|
||||
}
|
||||
|
||||
func executeShellCommand(ctx context.Context, command string) (string, error) {
|
||||
parts := strings.Fields(command)
|
||||
if len(parts) == 0 {
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||
)
|
||||
|
||||
// sshEnsureActionScript only deploys action.d/ui-custom-action.conf.
|
||||
// jail.local is managed by EnsureJailLocalStructure
|
||||
const sshEnsureActionScript = `python3 - <<'PY'
|
||||
import base64
|
||||
import pathlib
|
||||
@@ -30,40 +32,6 @@ try:
|
||||
action_dir.mkdir(parents=True, exist_ok=True)
|
||||
action_cfg = base64.b64decode("__PAYLOAD__").decode("utf-8")
|
||||
(action_dir / "ui-custom-action.conf").write_text(action_cfg)
|
||||
|
||||
jail_file = pathlib.Path("/etc/fail2ban/jail.local")
|
||||
if not jail_file.exists():
|
||||
jail_file.write_text("[DEFAULT]\n")
|
||||
|
||||
lines = jail_file.read_text().splitlines()
|
||||
already = any("Custom Fail2Ban action applied by fail2ban-ui" in line for line in lines)
|
||||
if not already:
|
||||
new_lines = []
|
||||
inserted = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("action") and "ui-custom-action" not in stripped and not inserted:
|
||||
if not stripped.startswith("#"):
|
||||
new_lines.append("# " + line)
|
||||
else:
|
||||
new_lines.append(line)
|
||||
new_lines.append("# Custom Fail2Ban action applied by fail2ban-ui")
|
||||
new_lines.append("action = %(action_mwlg)s")
|
||||
inserted = True
|
||||
continue
|
||||
new_lines.append(line)
|
||||
if not inserted:
|
||||
insert_at = None
|
||||
for idx, value in enumerate(new_lines):
|
||||
if value.strip().startswith("[DEFAULT]"):
|
||||
insert_at = idx + 1
|
||||
break
|
||||
if insert_at is None:
|
||||
new_lines.append("[DEFAULT]")
|
||||
insert_at = len(new_lines)
|
||||
new_lines.insert(insert_at, "# Custom Fail2Ban action applied by fail2ban-ui")
|
||||
new_lines.insert(insert_at + 1, "action = %(action_mwlg)s")
|
||||
jail_file.write_text("\n".join(new_lines) + "\n")
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"Error: {e}\n")
|
||||
sys.exit(1)
|
||||
@@ -1785,22 +1753,53 @@ PY`, escapeForShell(jailLocalPath), escapeForShell(ignoreIPStr), escapeForShell(
|
||||
return err
|
||||
}
|
||||
|
||||
// CheckJailLocalIntegrity implements Connector.
|
||||
func (sc *SSHConnector) CheckJailLocalIntegrity(ctx context.Context) (bool, bool, error) {
|
||||
const jailLocalPath = "/etc/fail2ban/jail.local"
|
||||
output, err := sc.runRemoteCommand(ctx, []string{"cat", jailLocalPath})
|
||||
if err != nil {
|
||||
// "No such file" means jail.local does not exist – that's fine
|
||||
if strings.Contains(err.Error(), "No such file") || strings.Contains(output, "No such file") {
|
||||
return false, false, nil
|
||||
}
|
||||
return false, false, fmt.Errorf("failed to read jail.local on %s: %w", sc.server.Name, err)
|
||||
}
|
||||
hasUIAction := strings.Contains(output, "ui-custom-action")
|
||||
return true, hasUIAction, nil
|
||||
}
|
||||
|
||||
// EnsureJailLocalStructure implements Connector.
|
||||
// For SSH connectors we:
|
||||
// 1. Migrate any legacy jails out of jail.local into jail.d/*.local
|
||||
// 2. Rebuild /etc/fail2ban/jail.local with a clean, managed structure
|
||||
// (banner, [DEFAULT] section based on current settings, and action_mwlg/action override).
|
||||
// If JAIL_AUTOMIGRATION=true, it first migrates any legacy jails to jail.d/.
|
||||
func (sc *SSHConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
||||
jailLocalPath := "/etc/fail2ban/jail.local"
|
||||
|
||||
// Check whether jail.local already exists and whether it belongs to us
|
||||
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)
|
||||
// Proceed cautiously; treat as "not ours" if the check itself failed.
|
||||
}
|
||||
if exists && !hasUI {
|
||||
// The file belongs to the user; never overwrite it.
|
||||
config.DebugLog("jail.local on server %s exists but is not managed by Fail2ban-UI - skipping overwrite", sc.server.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run experimental migration if enabled
|
||||
if isJailAutoMigrationEnabled() {
|
||||
config.DebugLog("JAIL_AUTOMIGRATION=true: running experimental jail.local → jail.d/ migration for SSH server %s", sc.server.Name)
|
||||
if err := sc.MigrateJailsFromJailLocalRemote(ctx); err != nil {
|
||||
return fmt.Errorf("failed to migrate legacy jails from jail.local on remote server %s: %w", sc.server.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Build the managed jail.local content
|
||||
settings := config.GetSettings()
|
||||
|
||||
// Convert IgnoreIPs array to space-separated string
|
||||
ignoreIPStr := strings.Join(settings.IgnoreIPs, " ")
|
||||
if ignoreIPStr == "" {
|
||||
ignoreIPStr = "127.0.0.1/8 ::1"
|
||||
}
|
||||
|
||||
// Set default banaction values if not set
|
||||
banactionVal := settings.Banaction
|
||||
if banactionVal == "" {
|
||||
banactionVal = "nftables-multiport"
|
||||
@@ -1814,7 +1813,6 @@ func (sc *SSHConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
||||
chainVal = "INPUT"
|
||||
}
|
||||
|
||||
// Build the new jail.local content in Go (mirrors local ensureJailLocalStructure)
|
||||
banner := config.JailLocalBanner()
|
||||
|
||||
defaultSection := fmt.Sprintf(`[DEFAULT]
|
||||
@@ -1859,15 +1857,6 @@ action = %(action_mwlg)s
|
||||
// Escape single quotes for safe use in a single-quoted heredoc
|
||||
escaped := strings.ReplaceAll(content, "'", "'\"'\"'")
|
||||
|
||||
// IMPORTANT: Run migration FIRST before ensuring structure.
|
||||
// This is because EnsureJailLocalStructure may overwrite jail.local,
|
||||
// which would destroy any jail sections that need to be migrated.
|
||||
// If migration fails for any reason, we SHOULD NOT overwrite jail.local,
|
||||
// otherwise legacy jails would be lost.
|
||||
if err := sc.MigrateJailsFromJailLocalRemote(ctx); err != nil {
|
||||
return fmt.Errorf("failed to migrate legacy jails from jail.local on remote server %s: %w", sc.server.Name, err)
|
||||
}
|
||||
|
||||
// Write the rebuilt content via heredoc over SSH
|
||||
writeScript := fmt.Sprintf(`cat > %s <<'JAILLOCAL'
|
||||
%s
|
||||
@@ -1879,6 +1868,7 @@ JAILLOCAL
|
||||
}
|
||||
|
||||
// MigrateJailsFromJailLocalRemote migrates non-commented jail sections from jail.local to jail.d/*.local files on remote system.
|
||||
// EXPERIMENTAL: Only called when JAIL_AUTOMIGRATION=true. It is always best to migrate a pre-existing jail.local by hand.
|
||||
func (sc *SSHConnector) MigrateJailsFromJailLocalRemote(ctx context.Context) error {
|
||||
jailLocalPath := "/etc/fail2ban/jail.local"
|
||||
jailDPath := "/etc/fail2ban/jail.d"
|
||||
@@ -1946,7 +1936,7 @@ func (sc *SSHConnector) MigrateJailsFromJailLocalRemote(ctx context.Context) err
|
||||
writeScript := fmt.Sprintf(`cat > %s <<'JAILEOF'
|
||||
%s
|
||||
JAILEOF
|
||||
'`, jailFilePath, escapedContent)
|
||||
`, jailFilePath, escapedContent)
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{writeScript}); err != nil {
|
||||
return fmt.Errorf("failed to write jail file %s: %w", jailFilePath, err)
|
||||
}
|
||||
@@ -1962,7 +1952,7 @@ JAILEOF
|
||||
writeLocalScript := fmt.Sprintf(`cat > %s <<'LOCALEOF'
|
||||
%s
|
||||
LOCALEOF
|
||||
'`, jailLocalPath, escapedDefault)
|
||||
`, jailLocalPath, escapedDefault)
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{writeLocalScript}); err != nil {
|
||||
return fmt.Errorf("failed to rewrite jail.local: %w", err)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ var (
|
||||
migrationOnce sync.Once
|
||||
)
|
||||
|
||||
// Auto-migration of an existing jail.local into jail.d/ is experimental and disabled by default;
|
||||
// it is always best to migrate a pre-existing jail.local by hand.
|
||||
func isJailAutoMigrationEnabled() bool {
|
||||
return strings.EqualFold(os.Getenv("JAIL_AUTOMIGRATION"), "true")
|
||||
}
|
||||
|
||||
// ensureJailLocalFile ensures that a .local file exists for the given jail.
|
||||
// If .local doesn't exist, it copies from .conf if available, or creates a minimal section.
|
||||
func ensureJailLocalFile(jailName string) error {
|
||||
@@ -324,16 +330,17 @@ func DeleteJail(jailName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllJails reads jails from /etc/fail2ban/jail.local (DEFAULT only) and /etc/fail2ban/jail.d directory.
|
||||
// Automatically migrates legacy jails from jail.local to jail.d on first call.
|
||||
// Now uses DiscoverJailsFromFiles() for file-based discovery.
|
||||
// GetAllJails reads jails from /etc/fail2ban/jail.d directory.
|
||||
func GetAllJails() ([]JailInfo, error) {
|
||||
// Run migration once if needed
|
||||
migrationOnce.Do(func() {
|
||||
if err := MigrateJailsToJailD(); err != nil {
|
||||
config.DebugLog("Migration warning: %v", err)
|
||||
}
|
||||
})
|
||||
// Run migration once if enabled (experimental, off by default)
|
||||
if isJailAutoMigrationEnabled() {
|
||||
migrationOnce.Do(func() {
|
||||
config.DebugLog("JAIL_AUTOMIGRATION=true: running experimental jail.local → jail.d/ migration")
|
||||
if err := MigrateJailsFromJailLocal(); err != nil {
|
||||
config.DebugLog("Migration warning: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Discover jails from filesystem
|
||||
jails, err := DiscoverJailsFromFiles()
|
||||
@@ -510,142 +517,9 @@ func UpdateJailEnabledStates(updates map[string]bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateJailsToJailD migrates all non-DEFAULT jails from jail.local to individual files in jail.d/.
|
||||
// Creates a backup of jail.local before migration. If a jail already exists in jail.d, jail.local takes precedence.
|
||||
func MigrateJailsToJailD() error {
|
||||
localPath := "/etc/fail2ban/jail.local"
|
||||
jailDPath := "/etc/fail2ban/jail.d"
|
||||
|
||||
// Check if jail.local exists
|
||||
if _, err := os.Stat(localPath); os.IsNotExist(err) {
|
||||
return nil // Nothing to migrate
|
||||
}
|
||||
|
||||
// Read jail.local content
|
||||
content, err := os.ReadFile(localPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read jail.local: %w", err)
|
||||
}
|
||||
|
||||
// Parse content to extract sections
|
||||
sections, defaultContent, err := parseJailSections(string(content))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse jail.local: %w", err)
|
||||
}
|
||||
|
||||
// If no non-DEFAULT jails found, nothing to migrate
|
||||
if len(sections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create backup of jail.local
|
||||
backupPath := localPath + ".backup." + fmt.Sprintf("%d", os.Getpid())
|
||||
if err := os.WriteFile(backupPath, content, 0644); err != nil {
|
||||
return fmt.Errorf("failed to create backup: %w", err)
|
||||
}
|
||||
config.DebugLog("Created backup of jail.local at %s", backupPath)
|
||||
|
||||
// Ensure jail.d directory exists
|
||||
if err := os.MkdirAll(jailDPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create jail.d directory: %w", err)
|
||||
}
|
||||
|
||||
// Write each jail to its own file in jail.d/
|
||||
for jailName, jailContent := range sections {
|
||||
jailFilePath := filepath.Join(jailDPath, jailName+".conf")
|
||||
|
||||
// Check if file already exists
|
||||
if _, err := os.Stat(jailFilePath); err == nil {
|
||||
// File exists - jail.local takes precedence, so overwrite
|
||||
config.DebugLog("Overwriting existing jail file %s with content from jail.local", jailFilePath)
|
||||
}
|
||||
|
||||
// Write jail content to file
|
||||
if err := os.WriteFile(jailFilePath, []byte(jailContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write jail file %s: %w", jailFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite jail.local with only DEFAULT section
|
||||
newLocalContent := defaultContent
|
||||
if !strings.HasSuffix(newLocalContent, "\n") {
|
||||
newLocalContent += "\n"
|
||||
}
|
||||
if err := os.WriteFile(localPath, []byte(newLocalContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to rewrite jail.local: %w", err)
|
||||
}
|
||||
|
||||
config.DebugLog("Migration completed: moved %d jails to jail.d/", len(sections))
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseJailSections parses jail.local content and returns:
|
||||
// - map of jail name to jail content (excluding DEFAULT and INCLUDES)
|
||||
// - DEFAULT section content
|
||||
func parseJailSections(content string) (map[string]string, string, error) {
|
||||
sections := make(map[string]string)
|
||||
var defaultContent strings.Builder
|
||||
|
||||
// Sections that should be ignored (not jails)
|
||||
ignoredSections := map[string]bool{
|
||||
"DEFAULT": true,
|
||||
"INCLUDES": true,
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
var currentSection string
|
||||
var currentContent strings.Builder
|
||||
inDefault := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
// Save previous section
|
||||
if currentSection != "" {
|
||||
sectionContent := strings.TrimSpace(currentContent.String())
|
||||
if inDefault {
|
||||
defaultContent.WriteString(sectionContent)
|
||||
if !strings.HasSuffix(sectionContent, "\n") {
|
||||
defaultContent.WriteString("\n")
|
||||
}
|
||||
} else if !ignoredSections[currentSection] {
|
||||
// Only save if it's not an ignored section
|
||||
sections[currentSection] = sectionContent
|
||||
}
|
||||
}
|
||||
|
||||
// Start new section
|
||||
currentSection = strings.Trim(trimmed, "[]")
|
||||
currentContent.Reset()
|
||||
currentContent.WriteString(line)
|
||||
currentContent.WriteString("\n")
|
||||
inDefault = (currentSection == "DEFAULT")
|
||||
} else {
|
||||
currentContent.WriteString(line)
|
||||
currentContent.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Save final section
|
||||
if currentSection != "" {
|
||||
sectionContent := strings.TrimSpace(currentContent.String())
|
||||
if inDefault {
|
||||
defaultContent.WriteString(sectionContent)
|
||||
} else if !ignoredSections[currentSection] {
|
||||
// Only save if it's not an ignored section
|
||||
sections[currentSection] = sectionContent
|
||||
}
|
||||
}
|
||||
|
||||
return sections, defaultContent.String(), scanner.Err()
|
||||
}
|
||||
|
||||
// parseJailSectionsUncommented parses jail.local content and returns:
|
||||
// - map of jail name to jail content (excluding DEFAULT, INCLUDES, and commented sections)
|
||||
// - DEFAULT section content (including commented lines)
|
||||
// Only extracts non-commented jail sections
|
||||
func parseJailSectionsUncommented(content string) (map[string]string, string, error) {
|
||||
sections := make(map[string]string)
|
||||
var defaultContent strings.Builder
|
||||
@@ -725,7 +599,7 @@ func parseJailSectionsUncommented(content string) (map[string]string, string, er
|
||||
}
|
||||
|
||||
// MigrateJailsFromJailLocal migrates non-commented jail sections from jail.local to jail.d/*.local files.
|
||||
// This should be called when a server is added or enabled to migrate legacy jails.
|
||||
// EXPERIMENTAL: Only called when JAIL_AUTOMIGRATION=true. It is always best to migrate a pre-existing jail.local by hand.
|
||||
func MigrateJailsFromJailLocal() error {
|
||||
localPath := "/etc/fail2ban/jail.local"
|
||||
jailDPath := "/etc/fail2ban/jail.d"
|
||||
|
||||
@@ -43,6 +43,10 @@ type Connector interface {
|
||||
// Jail local structure management
|
||||
EnsureJailLocalStructure(ctx context.Context) error
|
||||
|
||||
// CheckJailLocalIntegrity checks whether jail.local exists and contains the
|
||||
// ui-custom-action marker, which indicates it is managed by Fail2ban-UI.
|
||||
CheckJailLocalIntegrity(ctx context.Context) (bool, bool, error)
|
||||
|
||||
// Jail and filter creation/deletion
|
||||
CreateJail(ctx context.Context, jailName, content string) error
|
||||
DeleteJail(ctx context.Context, jailName string) error
|
||||
@@ -176,11 +180,13 @@ func updateConnectorAction(ctx context.Context, conn Connector) error {
|
||||
func newConnectorForServer(server config.Fail2banServer) (Connector, error) {
|
||||
switch server.Type {
|
||||
case "local":
|
||||
// IMPORTANT: Run migration FIRST before ensuring structure
|
||||
// This ensures any legacy jails in jail.local are migrated to jail.d/*.local
|
||||
// before ensureJailLocalStructure() overwrites jail.local
|
||||
if err := MigrateJailsFromJailLocal(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialise local fail2ban connector for %s: %w", server.Name, err)
|
||||
// Run migration FIRST before ensuring structure — but only when
|
||||
// the experimental JAIL_AUTOMIGRATION=true env var is set.
|
||||
if isJailAutoMigrationEnabled() {
|
||||
config.DebugLog("JAIL_AUTOMIGRATION=true: running experimental jail.local → jail.d/ migration for local server %s", server.Name)
|
||||
if err := MigrateJailsFromJailLocal(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialise local fail2ban connector for %s: %w", server.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.EnsureLocalFail2banAction(server); err != nil {
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"dashboard.loading_summary": "Zusammenfassung wird geladen…",
|
||||
"dashboard.no_enabled_servers_title": "Keine aktiven Verbindungen",
|
||||
"dashboard.no_enabled_servers_body": "Aktiviere den lokalen Connector oder registriere einen entfernten Fail2ban-Server, um Live-Daten zu sehen.",
|
||||
"dashboard.jail_local_warning_title": "Das jail.local auf diesem Server wird nicht von Fail2ban-UI verwaltet",
|
||||
"dashboard.jail_local_warning_body": "Die Datei /etc/fail2ban/jail.local auf dem ausgewählten Server existiert, wird aber nicht von Fail2ban-UI verwaltet. Die Callback-Aktion (ui-custom-action) fehlt, weshalb Ban-/Unban-Ereignisse nicht aufgezeichnet und keine E-Mail-Benachrichtigungen gesendet werden. Um dies zu beheben, verschiebe andernfalls selbst erstellte oder vorhandene Jail-Definitionen aus jail.local in eine eigene Datei unter /etc/fail2ban/jail.d/ (verwende jeweils jailname.conf für eine Standard-Konfiguration oder jailname.local um eine bestehende .conf zu überschreiben). Lösche danach jail.local, damit Fail2ban-UI die verwaltete Version erstellen kann (Zum erstellen, einmal in den Einstellungen auf speichern klichen). Stelle zudem sicher, dass Fail2ban-UI Schreibrechte auf /etc/fail2ban/ hat -> siehe Dokumentation.",
|
||||
"dashboard.errors.summary_failed": "Zusammenfassung konnte nicht vom Server geladen werden.",
|
||||
"dashboard.cards.active_jails": "Aktive Jails",
|
||||
"dashboard.cards.total_banned": "Gesamt gesperrte IPs",
|
||||
@@ -336,6 +338,7 @@
|
||||
"servers.actions.test": "Verbindung testen",
|
||||
"servers.actions.test_success": "Verbindung erfolgreich",
|
||||
"servers.actions.test_failure": "Verbindung fehlgeschlagen",
|
||||
"servers.jail_local_warning": "Warnung: jail.local wird nicht von Fail2ban-UI verwaltet. Verschiebe jeden Jail in eine eigene Datei unter jail.d/ und lösche jail.local, damit Fail2ban-UI sie neu erstellen kann (einmal auf der Einstellungen-Seite speichern, um die Datei zu schreiben). Siehe Dokumentation für Berechtigungen.",
|
||||
"servers.actions.restart": "Fail2ban neu starten",
|
||||
"servers.actions.reload": "Fail2ban neu laden",
|
||||
"servers.actions.reload_tooltip": "Für lokale Connectors ist nur ein Neuladen der Konfiguration über die Socket-Verbindung möglich. Der Container kann den Fail2ban-Dienst nicht mit systemctl neu starten. Für einen vollständigen Neustart führen Sie 'systemctl restart fail2ban' direkt auf dem Host-System aus.",
|
||||
@@ -344,7 +347,7 @@
|
||||
"servers.form.select_key": "Privaten Schlüssel auswählen",
|
||||
"servers.form.select_key_placeholder": "Manuelle Eingabe",
|
||||
"servers.form.no_keys": "Keine SSH-Schlüssel gefunden; Pfad manuell eingeben",
|
||||
"filter_debug.not_available": "Filter-Debug ist nur für lokale Connectoren verfügbar.",
|
||||
"filter_debug.not_available": "Filter-Debug ist nur verfügbar, wenn mindestens ein registrierter Fail2ban-Server aktiviert ist.",
|
||||
"filter_debug.local_missing": "Das lokale Fail2ban-Filterverzeichnis wurde auf diesem Host nicht gefunden.",
|
||||
"email.ban.title": "Achtung: Fail2Ban hat eine neue IP-Adresse blockiert",
|
||||
"email.ban.intro": "Fail2Ban-UI hat eine fehlerhafte Anfrage oder wiederholte Authentifizierungsfehler erkannt und die Quell-IP automatisch blockiert. Überprüfen Sie die Metadaten und Log-Auszüge unten.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"dashboard.loading_summary": "Laded Zämmefassig…",
|
||||
"dashboard.no_enabled_servers_title": "Ke aktivi Verbindige",
|
||||
"dashboard.no_enabled_servers_body": "Aktivier dr lokal Connector oder registrier ä entfernte Fail2ban-Server.",
|
||||
"dashboard.jail_local_warning_title": "Das jail.local uf däm Server wird nid vom Fail2ban-UI verwaltet",
|
||||
"dashboard.jail_local_warning_body": "D Datei /etc/fail2ban/jail.local ufem usgwählte Server existiert, wird aber nid vom Fail2ban-UI verwaltet. D Callback-Aktion (ui-custom-action) fähut, drum wärde Ban-/Unban-Ereignis nid ufzeichnet und kei E-Mail-Benachrichtigunge verschickt. Zum behebe: Verschieb jedi Jail-Definition us jail.local i ä eigeni Datei unger /etc/fail2ban/jail.d/ (nimm jailname.conf für ä Standard-Konfig oder jailname.local zumnä bestehendi .conf z überschriibe). Lösch denn jail.local, damit Fail2ban-UI ä eigeni verwalteti Version cha ersteue. Steu o sicher dass z Fail2ban-UI Schriibrächt uf /etc/fail2ban/ het -> siehe Doku.",
|
||||
"dashboard.errors.summary_failed": "Zämmefassig het nid chönne glade wärde.",
|
||||
"dashboard.cards.active_jails": "Aktivi Jails",
|
||||
"dashboard.cards.total_banned": "Total g'sperrti IPs",
|
||||
@@ -336,6 +338,7 @@
|
||||
"servers.actions.test": "Verbindig teste",
|
||||
"servers.actions.test_success": "Verbindig erfolgriich",
|
||||
"servers.actions.test_failure": "Verbindig nöd möglich",
|
||||
"servers.jail_local_warning": "Achtung: z jail.local wird nid vom Fail2ban-UI verwaltet. Verschieb jede Jail in ä eigeni Datei under jail.d/ und lösch z jail.local, damit Fail2ban-UI z file neu cha erstelle. Lueg d Doku a betreffend Berechtigunge.",
|
||||
"servers.actions.restart": "Fail2ban neu starte",
|
||||
"servers.actions.reload": "Fail2ban neu lade",
|
||||
"servers.actions.reload_tooltip": "Für lokali Connectors isch nur es Neulade vo de Konfiguration über d Socket-Verbindig möglich. Dr Container cha dr Fail2ban-Dienst nid mit systemctl neu starte. Für en vollständige Neustart füehre Sie 'systemctl restart fail2ban' direkt uf em Host-System us.",
|
||||
@@ -344,7 +347,7 @@
|
||||
"servers.form.select_key": "Priväte Schlissel ufwähle",
|
||||
"servers.form.select_key_placeholder": "Manuäll igäh",
|
||||
"servers.form.no_keys": "Kei SSH-Schlüssel gfunde; Pfad selber igäh",
|
||||
"filter_debug.not_available": "Filter-Debug git's nur für lokal Connectorä.",
|
||||
"filter_debug.not_available": "Filter-Debug funktioniert nur, we mindestens ei registrierte Fail2ban-Server aktiviert isch.",
|
||||
"filter_debug.local_missing": "S lokale Fail2ban-Filterverzeichnis isch uf däm Host nid gfunde worde.",
|
||||
"email.ban.title": "Achtung: Fail2Ban het e nöi IP-Adrässe blockiert",
|
||||
"email.ban.intro": "Fail2Ban-UI het e fehlerhafti Aafrag oder widerholti Authentifizierigsfähler erkennt und d Quell-IP automatisch blockiert. Überprüef d Metadate und Log-Uuszüg unge.",
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"dashboard.errors.summary_failed": "Failed to load summary from server.",
|
||||
"dashboard.no_enabled_servers_title": "No active connectors",
|
||||
"dashboard.no_enabled_servers_body": "Enable the local connector or register a remote Fail2ban server to see live data.",
|
||||
"dashboard.jail_local_warning_title": "The jail.local on this server is not managed by Fail2ban-UI",
|
||||
"dashboard.jail_local_warning_body": "The file /etc/fail2ban/jail.local on the selected server exists but is not managed by Fail2ban-UI. The callback action (ui-custom-action) is missing, which means ban/unban events will not be recorded and no email alerts will be sent. To fix this, move each jail section from your jail.local into its own file under /etc/fail2ban/jail.d/ (use jailname.conf to keep a default or jailname.local to override an existing .conf). Then delete jail.local so Fail2ban-UI can create its own managed version (hit once save on the settings page to write the file). Ensure also Fail2ban-UI has write permissions to /etc/fail2ban/ -> see the documentation for details.",
|
||||
"dashboard.cards.active_jails": "Active Jails",
|
||||
"dashboard.cards.total_banned": "Total Banned IPs",
|
||||
"dashboard.cards.new_last_hour": "New Last Hour",
|
||||
@@ -336,6 +338,7 @@
|
||||
"servers.actions.test": "Test connection",
|
||||
"servers.actions.test_success": "Connection successful",
|
||||
"servers.actions.test_failure": "Connection failed",
|
||||
"servers.jail_local_warning": "Warning: jail.local is not managed by Fail2ban-UI. Move each jail into its own file under jail.d/ and delete jail.local so Fail2ban-UI can recreate it (hit once save on the settings page to write the file). See docs for permissions.",
|
||||
"servers.actions.restart": "Restart Fail2ban",
|
||||
"servers.actions.reload": "Reload Fail2ban",
|
||||
"servers.actions.reload_tooltip": "For local connectors, only a configuration reload is possible via the socket connection. The container cannot restart the Fail2ban service using systemctl. To perform a full restart, run 'systemctl restart fail2ban' directly on the host system.",
|
||||
@@ -344,7 +347,7 @@
|
||||
"servers.form.select_key": "Select Private Key",
|
||||
"servers.form.select_key_placeholder": "Manual entry",
|
||||
"servers.form.no_keys": "No SSH keys found; enter path manually",
|
||||
"filter_debug.not_available": "Filter debug is only available for local connectors.",
|
||||
"filter_debug.not_available": "Filter debug is only available when at least one registered Fail2ban server is enabled.",
|
||||
"filter_debug.local_missing": "The local Fail2ban filter directory was not found on this host.",
|
||||
"email.ban.title": "Security alert: Fail2Ban blocked a new IP-address",
|
||||
"email.ban.intro": "Fail2Ban-UI detected a bad request or repeated authentication failures and automatically blocked the source-IP. Review the metadata and log excerpts below.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"dashboard.loading_summary": "Cargando resumen…",
|
||||
"dashboard.no_enabled_servers_title": "Sin conectores activos",
|
||||
"dashboard.no_enabled_servers_body": "Activa el conector local o registra un servidor Fail2ban remoto para ver datos en vivo.",
|
||||
"dashboard.jail_local_warning_title": "El jail.local en este servidor no está gestionado por Fail2ban-UI",
|
||||
"dashboard.jail_local_warning_body": "El archivo /etc/fail2ban/jail.local en el servidor seleccionado existe pero no está gestionado por Fail2ban-UI. Falta la acción de callback (ui-custom-action), lo que significa que los eventos de ban/unban no se registrarán y no se enviarán alertas por correo electrónico. Para solucionarlo, mueva cada sección jail de jail.local a su propio archivo en /etc/fail2ban/jail.d/ (use jailname.conf para mantener una configuración por defecto o jailname.local para anular un .conf existente). Luego elimine jail.local para que Fail2ban-UI pueda crear su propia versión gestionada (pulse una vez en la página de configuración para escribir el archivo). Asegúrese de que Fail2ban-UI tenga permisos de escritura en /etc/fail2ban/ -> consulte la documentación.",
|
||||
"dashboard.errors.summary_failed": "No se pudo cargar el resumen desde el servidor.",
|
||||
"dashboard.cards.active_jails": "Jails activos",
|
||||
"dashboard.cards.total_banned": "IPs bloqueadas totales",
|
||||
@@ -336,6 +338,7 @@
|
||||
"servers.actions.test": "Probar conexión",
|
||||
"servers.actions.test_success": "Conexión exitosa",
|
||||
"servers.actions.test_failure": "Conexión fallida",
|
||||
"servers.jail_local_warning": "Advertencia: jail.local no está gestionado por Fail2ban-UI. Mueva cada jail a su propio archivo en jail.d/ y elimine jail.local para que Fail2ban-UI pueda recrearlo (pulse una vez en la página de configuración para escribir el archivo). Consulte la documentación para permisos.",
|
||||
"servers.actions.restart": "Reiniciar Fail2ban",
|
||||
"servers.actions.reload": "Recargar Fail2ban",
|
||||
"servers.actions.reload_tooltip": "Para los conectores locales, solo es posible recargar la configuración a través de la conexión socket. El contenedor no puede reiniciar el servicio Fail2ban usando systemctl. Para realizar un reinicio completo, ejecute 'systemctl restart fail2ban' directamente en el sistema host.",
|
||||
@@ -344,7 +347,7 @@
|
||||
"servers.form.select_key": "Seleccionar clave privada",
|
||||
"servers.form.select_key_placeholder": "Entrada manual",
|
||||
"servers.form.no_keys": "No se encontraron claves SSH; introduzca la ruta manualmente",
|
||||
"filter_debug.not_available": "La depuración de filtros solo está disponible para conectores locales.",
|
||||
"filter_debug.not_available": "La depuración de filtros solo está disponible cuando al menos un servidor Fail2ban registrado está activado.",
|
||||
"filter_debug.local_missing": "No se encontró el directorio de filtros local de Fail2ban en este host.",
|
||||
"email.ban.title": "Alerta de seguridad: Fail2Ban bloqueó una nueva dirección IP",
|
||||
"email.ban.intro": "Fail2Ban-UI detectó una solicitud incorrecta o fallos de autenticación repetidos y bloqueó automáticamente la IP de origen. Revise los metadatos y extractos de registro a continuación.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"dashboard.loading_summary": "Chargement du résumé…",
|
||||
"dashboard.no_enabled_servers_title": "Aucun connecteur actif",
|
||||
"dashboard.no_enabled_servers_body": "Activez le connecteur local ou enregistrez un serveur Fail2ban distant pour voir les données en direct.",
|
||||
"dashboard.jail_local_warning_title": "Le jail.local sur ce serveur n'est pas géré par Fail2ban-UI",
|
||||
"dashboard.jail_local_warning_body": "Le fichier /etc/fail2ban/jail.local sur le serveur sélectionné existe mais n'est pas géré par Fail2ban-UI. L'action de rappel (ui-custom-action) est manquante, ce qui signifie que les événements ban/unban ne seront pas enregistrés et aucune alerte e-mail ne sera envoyée. Pour corriger cela, déplacez chaque section jail de jail.local dans son propre fichier sous /etc/fail2ban/jail.d/ (utilisez jailname.conf pour garder une configuration par défaut ou jailname.local pour remplacer un .conf existant). Supprimez ensuite jail.local pour que Fail2ban-UI puisse créer sa propre version gérée (cliquez une fois sur la page de configuration pour écrire le fichier). Assurez-vous que Fail2ban-UI dispose des droits d'écriture sur /etc/fail2ban/ -> consultez la documentation.",
|
||||
"dashboard.errors.summary_failed": "Impossible de charger le résumé depuis le serveur.",
|
||||
"dashboard.cards.active_jails": "Jails actifs",
|
||||
"dashboard.cards.total_banned": "Total d'IPs bloquées",
|
||||
@@ -336,6 +338,7 @@
|
||||
"servers.actions.test": "Tester la connexion",
|
||||
"servers.actions.test_success": "Connexion réussie",
|
||||
"servers.actions.test_failure": "Échec de la connexion",
|
||||
"servers.jail_local_warning": "Attention : jail.local n'est pas géré par Fail2ban-UI. Déplacez chaque jail dans son propre fichier sous jail.d/ et supprimez jail.local pour que Fail2ban-UI puisse le recréer (cliquez une fois sur la page de configuration pour écrire le fichier). Consultez la documentation pour les permissions.",
|
||||
"servers.actions.restart": "Redémarrer Fail2ban",
|
||||
"servers.actions.reload": "Recharger Fail2ban",
|
||||
"servers.actions.reload_tooltip": "Pour les connecteurs locaux, seul un rechargement de la configuration est possible via la connexion socket. Le conteneur ne peut pas redémarrer le service Fail2ban en utilisant systemctl. Pour effectuer un redémarrage complet, exécutez 'systemctl restart fail2ban' directement sur le système hôte.",
|
||||
@@ -344,7 +347,7 @@
|
||||
"servers.form.select_key": "Sélectionner la clé privée",
|
||||
"servers.form.select_key_placeholder": "Saisie manuelle",
|
||||
"servers.form.no_keys": "Aucune clé SSH trouvée ; saisissez le chemin manuellement",
|
||||
"filter_debug.not_available": "Le débogage des filtres n'est disponible que pour les connecteurs locaux.",
|
||||
"filter_debug.not_available": "Le débogage des filtres n'est disponible que lorsqu'au moins un serveur Fail2ban enregistré est activé.",
|
||||
"filter_debug.local_missing": "Le répertoire de filtres Fail2ban local est introuvable sur cet hôte.",
|
||||
"email.ban.title": "Alerte de sécurité : Fail2Ban a bloqué une nouvelle adresse IP",
|
||||
"email.ban.intro": "Fail2Ban-UI a détecté une requête suspecte ou des échecs d'authentification répétés et a automatiquement bloqué l'IP source. Consultez les métadonnées et extraits de journaux ci-dessous.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"dashboard.loading_summary": "Caricamento del riepilogo…",
|
||||
"dashboard.no_enabled_servers_title": "Nessun connettore attivo",
|
||||
"dashboard.no_enabled_servers_body": "Abilita il connettore locale o registra un server Fail2ban remoto per visualizzare dati in tempo reale.",
|
||||
"dashboard.jail_local_warning_title": "Il jail.local sul server selezionato non è gestito da Fail2ban-UI",
|
||||
"dashboard.jail_local_warning_body": "Il file /etc/fail2ban/jail.local sul server selezionato esiste ma non è gestito da Fail2ban-UI. L'azione di callback (ui-custom-action) è assente, il che significa che gli eventi ban/unban non verranno registrati e non verranno inviate notifiche e-mail. Per risolvere, sposta ogni sezione jail da jail.local in un file proprio sotto /etc/fail2ban/jail.d/ (usa jailname.conf per mantenere una configurazione predefinita o jailname.local per sovrascrivere un .conf esistente). Poi elimina jail.local in modo che Fail2ban-UI possa creare la propria versione gestita (salva una volta la pagina delle impostazioni per scrivere il file). Assicurati che Fail2ban-UI abbia i permessi di scrittura su /etc/fail2ban/ -> consulta la documentazione.",
|
||||
"dashboard.errors.summary_failed": "Impossibile caricare il riepilogo dal server.",
|
||||
"dashboard.cards.active_jails": "Jail attivi",
|
||||
"dashboard.cards.total_banned": "IP bloccate totali",
|
||||
@@ -336,6 +338,7 @@
|
||||
"servers.actions.test": "Verifica connessione",
|
||||
"servers.actions.test_success": "Connessione riuscita",
|
||||
"servers.actions.test_failure": "Connessione fallita",
|
||||
"servers.jail_local_warning": "Attenzione: jail.local non è gestito da Fail2ban-UI. Sposta ogni jail in un file proprio sotto jail.d/ ed elimina jail.local in modo che Fail2ban-UI possa ricrearlo (salva una volta la pagina delle impostazioni per scrivere il file). Consulta la documentazione per i permessi.",
|
||||
"servers.actions.restart": "Riavvia Fail2ban",
|
||||
"servers.actions.reload": "Ricarica Fail2ban",
|
||||
"servers.actions.reload_tooltip": "Per i connettori locali, è possibile solo un ricaricamento della configurazione tramite la connessione socket. Il contenitore non può riavviare il servizio Fail2ban utilizzando systemctl. Per eseguire un riavvio completo, eseguire 'systemctl restart fail2ban' direttamente sul sistema host.",
|
||||
@@ -344,7 +347,7 @@
|
||||
"servers.form.select_key": "Seleziona chiave privata",
|
||||
"servers.form.select_key_placeholder": "Inserimento manuale",
|
||||
"servers.form.no_keys": "Nessuna chiave SSH trovata; inserire il percorso manualmente",
|
||||
"filter_debug.not_available": "Il debug dei filtri è disponibile solo per i connettori locali.",
|
||||
"filter_debug.not_available": "Il debug dei filtri è disponibile solo quando almeno un server Fail2ban registrato è attivato.",
|
||||
"filter_debug.local_missing": "La directory dei filtri Fail2ban locale non è stata trovata su questo host.",
|
||||
"email.ban.title": "Allerta di sicurezza: Fail2Ban ha bloccato un nuovo indirizzo IP",
|
||||
"email.ban.intro": "Fail2Ban-UI ha rilevato una richiesta sospetta o ripetuti fallimenti di autenticazione e ha automaticamente bloccato l'IP sorgente. Rivedere i metadati e gli estratti di log di seguito.",
|
||||
|
||||
@@ -63,7 +63,8 @@ func SetWebSocketHub(hub *Hub) {
|
||||
|
||||
// SummaryResponse is what we return from /api/summary
|
||||
type SummaryResponse struct {
|
||||
Jails []fail2ban.JailInfo `json:"jails"`
|
||||
Jails []fail2ban.JailInfo `json:"jails"`
|
||||
JailLocalWarning bool `json:"jailLocalWarning,omitempty"`
|
||||
}
|
||||
|
||||
type emailDetail struct {
|
||||
@@ -150,6 +151,13 @@ func SummaryHandler(c *gin.Context) {
|
||||
resp := SummaryResponse{
|
||||
Jails: jailInfos,
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -595,41 +603,53 @@ func UpsertServerHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Ensure jail.local structure is properly initialized for newly enabled/added servers
|
||||
var jailLocalWarning bool
|
||||
if justEnabled || !wasEnabled {
|
||||
conn, err := fail2ban.GetManager().Connector(server.ID)
|
||||
if err == nil {
|
||||
// EnsureJailLocalStructure respects user-owned files:
|
||||
// - file missing --> creates it
|
||||
// - file is ours --> updates it
|
||||
// - file is user's own --> leave it alone
|
||||
if err := conn.EnsureJailLocalStructure(c.Request.Context()); err != nil {
|
||||
config.DebugLog("Warning: failed to ensure jail.local structure for server %s: %v", server.Name, err)
|
||||
// Don't fail the request, just log the warning
|
||||
} else {
|
||||
config.DebugLog("Successfully ensured jail.local structure for server %s", server.Name)
|
||||
}
|
||||
|
||||
// If the server was just enabled, try to restart fail2ban and perform a basic health check.
|
||||
if justEnabled {
|
||||
if err := conn.Restart(c.Request.Context()); err != nil {
|
||||
// Surface restart failures to the UI so the user sees that the service did not restart.
|
||||
msg := fmt.Sprintf("failed to restart fail2ban for server %s: %v", server.Name, err)
|
||||
config.DebugLog("Warning: %s", msg)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": msg,
|
||||
"server": server,
|
||||
})
|
||||
return
|
||||
// Check integrity AFTER ensuring structure so fresh servers don't
|
||||
// trigger a false-positive warning.
|
||||
if exists, hasUI, chkErr := conn.CheckJailLocalIntegrity(c.Request.Context()); chkErr == nil && exists && !hasUI {
|
||||
jailLocalWarning = true
|
||||
log.Printf("⚠️ Server %s: jail.local is not managed by Fail2ban-UI. Please migrate your jail.local manually (see documentation).", server.Name)
|
||||
}
|
||||
|
||||
// If the server was just enabled, try to restart fail2ban and perform a basic health check.
|
||||
if justEnabled {
|
||||
if err := conn.Restart(c.Request.Context()); err != nil {
|
||||
msg := fmt.Sprintf("failed to restart fail2ban for server %s: %v", server.Name, err)
|
||||
config.DebugLog("Warning: %s", msg)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": msg,
|
||||
"server": server,
|
||||
})
|
||||
return
|
||||
} else {
|
||||
if _, err := conn.GetJailInfos(c.Request.Context()); err != nil {
|
||||
config.DebugLog("Warning: fail2ban appears unhealthy on server %s after restart: %v", server.Name, err)
|
||||
} else {
|
||||
// Basic health check: attempt to fetch jail infos, which runs fail2ban-client status.
|
||||
if _, err := conn.GetJailInfos(c.Request.Context()); err != nil {
|
||||
config.DebugLog("Warning: fail2ban appears unhealthy on server %s after restart: %v", server.Name, err)
|
||||
// Again, we log instead of failing the request to avoid breaking existing flows.
|
||||
} else {
|
||||
config.DebugLog("Fail2ban service appears healthy on server %s after restart", server.Name)
|
||||
}
|
||||
config.DebugLog("Fail2ban service appears healthy on server %s after restart", server.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"server": server})
|
||||
resp := gin.H{"server": server}
|
||||
if jailLocalWarning {
|
||||
resp["jailLocalWarning"] = true
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// DeleteServerHandler removes a server configuration.
|
||||
@@ -760,7 +780,14 @@ func TestServerHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"messageKey": "servers.actions.test_success"})
|
||||
// Check jail.local integrity: if it exists but is not managed by fail2ban-ui, warn the user
|
||||
resp := gin.H{"messageKey": "servers.actions.test_success"}
|
||||
if exists, hasUI, err := conn.CheckJailLocalIntegrity(ctx); err == nil {
|
||||
if exists && !hasUI {
|
||||
resp["jailLocalWarning"] = true
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// HandleBanNotification processes Fail2Ban notifications, checks geo-location, stores the event, and sends alerts.
|
||||
|
||||
@@ -46,14 +46,17 @@ function fetchSummaryData() {
|
||||
if (data && !data.error) {
|
||||
latestSummary = data;
|
||||
latestSummaryError = null;
|
||||
jailLocalWarning = !!data.jailLocalWarning;
|
||||
} else {
|
||||
latestSummary = null;
|
||||
latestSummaryError = data && data.error ? data.error : t('dashboard.errors.summary_failed', 'Failed to load summary from server.');
|
||||
jailLocalWarning = false;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
latestSummary = null;
|
||||
latestSummaryError = err ? err.toString() : 'Unknown error';
|
||||
jailLocalWarning = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -597,6 +600,20 @@ function renderDashboard() {
|
||||
var summary = latestSummary;
|
||||
var html = '';
|
||||
|
||||
// Persistent warning banner when jail.local is not managed by Fail2ban-UI
|
||||
if (jailLocalWarning) {
|
||||
html += ''
|
||||
+ '<div class="bg-red-100 border-l-4 border-red-500 text-red-800 px-4 py-3 rounded mb-4 flex items-start gap-3" role="alert">'
|
||||
+ ' <svg class="w-5 h-5 mt-0.5 flex-shrink-0 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">'
|
||||
+ ' <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>'
|
||||
+ ' </svg>'
|
||||
+ ' <div>'
|
||||
+ ' <p class="font-semibold" data-i18n="dashboard.jail_local_warning_title">jail.local not managed by Fail2ban-UI</p>'
|
||||
+ ' <p class="text-sm mt-1" data-i18n="dashboard.jail_local_warning_body">The file /etc/fail2ban/jail.local on the selected server exists but is not managed by Fail2ban-UI. The callback action (ui-custom-action) is missing, which means ban/unban events will not be recorded and no email alerts will be sent. To fix this, move each jail section from jail.local into its own file under /etc/fail2ban/jail.d/ (use jailname.conf to keep a default or jailname.local to override an existing .conf). Then delete jail.local so Fail2ban-UI can create its own managed version. Ensure Fail2ban-UI has write permissions to /etc/fail2ban/ — see the documentation for details.</p>'
|
||||
+ ' </div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
if (latestSummaryError) {
|
||||
html += ''
|
||||
+ '<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">'
|
||||
|
||||
@@ -26,3 +26,4 @@ var translations = {};
|
||||
var sshKeysCache = null;
|
||||
var openModalCount = 0;
|
||||
var isLOTRModeActive = false;
|
||||
var jailLocalWarning = false;
|
||||
|
||||
@@ -190,7 +190,7 @@ function openBanInsightsModal() {
|
||||
percent = Math.min(Math.max(percent, 3), 100);
|
||||
return ''
|
||||
+ '<div class="space-y-2">'
|
||||
+ ' <div class="flex items-center justify-between text-sm font-medium text-gray-800" style="border-bottom: ridge;">'
|
||||
+ ' <div class="flex items-center justify-between text-sm font-medium text-gray-800">'
|
||||
+ ' <span>' + escapeHtml(label) + '</span>'
|
||||
+ ' <span>' + formatNumber(stat.count || 0) + '</span>'
|
||||
+ ' </div>'
|
||||
|
||||
@@ -354,6 +354,9 @@ function submitServerForm(event) {
|
||||
return;
|
||||
}
|
||||
showToast(t('servers.form.success', 'Server saved successfully.'), 'success');
|
||||
if (data.jailLocalWarning) {
|
||||
showToast(t('servers.jail_local_warning', 'Warning: jail.local is not managed by Fail2ban-UI. Move each jail into its own file under jail.d/ and delete jail.local so Fail2ban-UI can recreate it. See docs for permissions.'), 'warning', 12000);
|
||||
}
|
||||
var saved = data.server || {};
|
||||
currentServerId = saved.id || currentServerId;
|
||||
return loadServers().then(function() {
|
||||
@@ -475,6 +478,9 @@ function testServerConnection(serverId) {
|
||||
return;
|
||||
}
|
||||
showToast(t(data.messageKey || 'servers.actions.test_success', data.message || 'Connection successful'), 'success');
|
||||
if (data.jailLocalWarning) {
|
||||
showToast(t('servers.jail_local_warning', 'Warning: jail.local is not managed by Fail2ban-UI. Move each jail into its own file under jail.d/ and delete jail.local so Fail2ban-UI can recreate it. See docs for permissions.'), 'warning', 12000);
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
showToast(t('servers.actions.test_failure', 'Connection failed') + ': ' + err, 'error');
|
||||
|
||||
Reference in New Issue
Block a user