mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Fix remote SSH jail.local pharsing
This commit is contained in:
@@ -993,8 +993,8 @@ jail_file = '%s'
|
|||||||
ignore_ip_str = '%s'
|
ignore_ip_str = '%s'
|
||||||
banaction_val = '%s'
|
banaction_val = '%s'
|
||||||
banaction_allports_val = '%s'
|
banaction_allports_val = '%s'
|
||||||
default_jail_enable_val = %t
|
default_jail_enable_val = '%t'
|
||||||
bantime_increment_val = %t
|
bantime_increment_val = '%t'
|
||||||
bantime_val = '%s'
|
bantime_val = '%s'
|
||||||
findtime_val = '%s'
|
findtime_val = '%s'
|
||||||
maxretry_val = %d
|
maxretry_val = %d
|
||||||
@@ -1082,6 +1082,10 @@ PY`, escapeForShell(jailLocalPath), escapeForShell(ignoreIPStr), escapeForShell(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EnsureJailLocalStructure implements Connector.
|
// 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).
|
||||||
func (sc *SSHConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
func (sc *SSHConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
||||||
jailLocalPath := "/etc/fail2ban/jail.local"
|
jailLocalPath := "/etc/fail2ban/jail.local"
|
||||||
settings := config.GetSettings()
|
settings := config.GetSettings()
|
||||||
@@ -1091,6 +1095,7 @@ func (sc *SSHConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
|||||||
if ignoreIPStr == "" {
|
if ignoreIPStr == "" {
|
||||||
ignoreIPStr = "127.0.0.1/8 ::1"
|
ignoreIPStr = "127.0.0.1/8 ::1"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default banaction values if not set
|
// Set default banaction values if not set
|
||||||
banactionVal := settings.Banaction
|
banactionVal := settings.Banaction
|
||||||
if banactionVal == "" {
|
if banactionVal == "" {
|
||||||
@@ -1100,170 +1105,63 @@ func (sc *SSHConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
|||||||
if banactionAllportsVal == "" {
|
if banactionAllportsVal == "" {
|
||||||
banactionAllportsVal = "iptables-allports"
|
banactionAllportsVal = "iptables-allports"
|
||||||
}
|
}
|
||||||
// Escape values for shell/Python
|
|
||||||
escapeForShell := func(s string) string {
|
|
||||||
return strings.ReplaceAll(s, "'", "'\"'\"'")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the structure using Python script
|
// Build the new jail.local content in Go (mirrors local ensureJailLocalStructure)
|
||||||
ensureScript := fmt.Sprintf(`python3 <<'PY'
|
banner := config.JailLocalBanner()
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
jail_file = '%s'
|
defaultSection := fmt.Sprintf(`[DEFAULT]
|
||||||
ignore_ip_str = '%s'
|
enabled = %t
|
||||||
banaction_val = '%s'
|
bantime.increment = %t
|
||||||
banaction_allports_val = '%s'
|
ignoreip = %s
|
||||||
banner_content = """%s"""
|
bantime = %s
|
||||||
settings = {
|
findtime = %s
|
||||||
'bantime_increment': %t,
|
maxretry = %d
|
||||||
'default_jail_enable': %t,
|
destemail = %s
|
||||||
'ignoreip': ignore_ip_str,
|
banaction = %s
|
||||||
'bantime': '%s',
|
banaction_allports = %s
|
||||||
'findtime': '%s',
|
|
||||||
'maxretry': %d,
|
|
||||||
'destemail': '%s',
|
|
||||||
'banaction': banaction_val,
|
|
||||||
'banaction_allports': banaction_allports_val
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if file already has our full banner (indicating it's already properly structured)
|
`,
|
||||||
has_full_banner = False
|
settings.DefaultJailEnable,
|
||||||
has_action_mwlg = False
|
settings.BantimeIncrement,
|
||||||
has_action_override = False
|
ignoreIPStr,
|
||||||
|
settings.Bantime,
|
||||||
|
settings.Findtime,
|
||||||
|
settings.Maxretry,
|
||||||
|
settings.Destemail,
|
||||||
|
banactionVal,
|
||||||
|
banactionAllportsVal,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
actionMwlgConfig := `# Custom Fail2Ban action using geo-filter for email alerts
|
||||||
with open(jail_file, 'r') as f:
|
action_mwlg = %(action_)s
|
||||||
content = f.read()
|
ui-custom-action[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
|
||||||
# Check for the complete banner pattern with hash line separators
|
|
||||||
has_full_banner = '################################################################################' in content and 'Fail2Ban-UI Managed Configuration' in content and 'DO NOT EDIT THIS FILE MANUALLY' in content
|
|
||||||
has_action_mwlg = 'action_mwlg' in content and 'ui-custom-action' in content
|
|
||||||
has_action_override = 'action = %%(action_mwlg)s' in content
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# If already properly structured, just update DEFAULT section
|
`
|
||||||
if has_full_banner and has_action_mwlg and has_action_override:
|
|
||||||
try:
|
|
||||||
with open(jail_file, 'r') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
except FileNotFoundError:
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
# Always add the full banner at the start
|
|
||||||
output_lines = []
|
|
||||||
output_lines.extend(banner_content.splitlines())
|
|
||||||
output_lines.append('')
|
|
||||||
|
|
||||||
# Skip everything before [DEFAULT] section (old banner, comments, empty lines)
|
|
||||||
found_section = False
|
|
||||||
for line in lines:
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.startswith('[') and stripped.endswith(']'):
|
|
||||||
# Found a section - stop skipping and process this line
|
|
||||||
found_section = True
|
|
||||||
if not found_section:
|
|
||||||
# Skip lines before any section (old banner, comments, empty lines)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Process lines after we found a section
|
|
||||||
if stripped.startswith('[') and stripped.endswith(']'):
|
|
||||||
section_name = stripped.strip('[]')
|
|
||||||
if section_name == "DEFAULT":
|
|
||||||
in_default = True
|
|
||||||
output_lines.append(line)
|
|
||||||
else:
|
|
||||||
in_default = False
|
|
||||||
output_lines.append(line)
|
|
||||||
elif in_default:
|
|
||||||
key_updated = False
|
|
||||||
for key, new_value in [
|
|
||||||
('enabled', 'enabled = ' + str(settings['default_jail_enable'])),
|
|
||||||
('bantime.increment', 'bantime.increment = ' + str(settings['bantime_increment'])),
|
|
||||||
('ignoreip', 'ignoreip = ' + settings['ignoreip']),
|
|
||||||
('bantime', 'bantime = ' + settings['bantime']),
|
|
||||||
('findtime', 'findtime = ' + settings['findtime']),
|
|
||||||
('maxretry', 'maxretry = ' + str(settings['maxretry'])),
|
|
||||||
('destemail', 'destemail = ' + settings['destemail']),
|
|
||||||
('banaction', 'banaction = ' + settings['banaction']),
|
|
||||||
('banaction_allports', 'banaction_allports = ' + settings['banaction_allports']),
|
|
||||||
]:
|
|
||||||
pattern = r'^\s*' + re.escape(key) + r'\s*='
|
|
||||||
if re.match(pattern, stripped):
|
|
||||||
output_lines.append(new_value + '\n')
|
|
||||||
keys_updated.add(key)
|
|
||||||
key_updated = True
|
|
||||||
break
|
|
||||||
if not key_updated:
|
|
||||||
output_lines.append(line)
|
|
||||||
else:
|
|
||||||
output_lines.append(line)
|
|
||||||
|
|
||||||
# Add missing keys
|
|
||||||
if in_default:
|
|
||||||
for key, new_value in [
|
|
||||||
('enabled', 'enabled = ' + str(settings['default_jail_enable'])),
|
|
||||||
('bantime.increment', 'bantime.increment = ' + str(settings['bantime_increment'])),
|
|
||||||
('ignoreip', 'ignoreip = ' + settings['ignoreip']),
|
|
||||||
('bantime', 'bantime = ' + settings['bantime']),
|
|
||||||
('findtime', 'findtime = ' + settings['findtime']),
|
|
||||||
('maxretry', 'maxretry = ' + str(settings['maxretry'])),
|
|
||||||
('destemail', 'destemail = ' + settings['destemail']),
|
|
||||||
('banaction', 'banaction = ' + settings['banaction']),
|
|
||||||
('banaction_allports', 'banaction_allports = ' + settings['banaction_allports']),
|
|
||||||
]:
|
|
||||||
if key not in keys_updated:
|
|
||||||
for i, output_line in enumerate(output_lines):
|
|
||||||
if output_line.strip() == "[DEFAULT]":
|
|
||||||
output_lines.insert(i + 1, new_value + '\n')
|
|
||||||
break
|
|
||||||
|
|
||||||
with open(jail_file, 'w') as f:
|
|
||||||
f.writelines(output_lines)
|
|
||||||
else:
|
|
||||||
# Create new structure
|
|
||||||
banner = banner_content
|
|
||||||
|
|
||||||
default_section = """[DEFAULT]
|
|
||||||
enabled = """ + str(settings['default_jail_enable']) + """
|
|
||||||
bantime.increment = """ + str(settings['bantime_increment']) + """
|
|
||||||
ignoreip = """ + settings['ignoreip'] + """
|
|
||||||
bantime = """ + settings['bantime'] + """
|
|
||||||
findtime = """ + settings['findtime'] + """
|
|
||||||
maxretry = """ + str(settings['maxretry']) + """
|
|
||||||
destemail = """ + settings['destemail'] + """
|
|
||||||
banaction = """ + settings['banaction'] + """
|
|
||||||
banaction_allports = """ + settings['banaction_allports'] + """
|
|
||||||
|
|
||||||
"""
|
actionOverride := `# Custom Fail2Ban action applied by fail2ban-ui
|
||||||
|
action = %(action_mwlg)s
|
||||||
action_mwlg_config = """# Custom Fail2Ban action using geo-filter for email alerts
|
`
|
||||||
action_mwlg = %%(action_)s
|
|
||||||
ui-custom-action[sender="%%(sender)s", dest="%%(destemail)s", logpath="%%(logpath)s", chain="%%(chain)s"]
|
|
||||||
|
|
||||||
"""
|
content := banner + defaultSection + actionMwlgConfig + actionOverride
|
||||||
|
|
||||||
action_override = """# Custom Fail2Ban action applied by fail2ban-ui
|
// Escape single quotes for safe use in a single-quoted heredoc
|
||||||
action = %%(action_mwlg)s
|
escaped := strings.ReplaceAll(content, "'", "'\"'\"'")
|
||||||
"""
|
|
||||||
|
|
||||||
new_content = banner + default_section + action_mwlg_config + action_override
|
|
||||||
|
|
||||||
with open(jail_file, 'w') as f:
|
|
||||||
f.write(new_content)
|
|
||||||
PY`, escapeForShell(jailLocalPath), escapeForShell(ignoreIPStr), escapeForShell(banactionVal), escapeForShell(banactionAllportsVal), escapeForShell(config.JailLocalBanner()), settings.BantimeIncrement, settings.DefaultJailEnable,
|
|
||||||
escapeForShell(settings.Bantime), escapeForShell(settings.Findtime), settings.Maxretry, escapeForShell(settings.Destemail))
|
|
||||||
|
|
||||||
// IMPORTANT: Run migration FIRST before ensuring structure
|
// IMPORTANT: Run migration FIRST before ensuring structure
|
||||||
// This is because ensureJailLocalStructure may overwrite jail.local,
|
// This is because EnsureJailLocalStructure may overwrite jail.local,
|
||||||
// which would destroy any jail sections that need to be migrated
|
// which would destroy any jail sections that need to be migrated.
|
||||||
if err := sc.MigrateJailsFromJailLocalRemote(ctx); err != nil {
|
if err := sc.MigrateJailsFromJailLocalRemote(ctx); err != nil {
|
||||||
config.DebugLog("Warning: No migration done (may be normal if no jails to migrate): %v", err)
|
config.DebugLog("Warning: No migration done (may be normal if no jails to migrate): %v", err)
|
||||||
// Don't fail - continue with ensuring structure
|
// Don't fail - continue with ensuring structure
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then ensure the basic structure
|
// Write the rebuilt content via heredoc over SSH
|
||||||
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", ensureScript})
|
writeScript := fmt.Sprintf(`cat > %s <<'JAILLOCAL'
|
||||||
|
%s
|
||||||
|
JAILLOCAL
|
||||||
|
`, jailLocalPath, escaped)
|
||||||
|
|
||||||
|
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", writeScript})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user