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:
2026-02-09 19:56:43 +01:00
parent e8592d17e6
commit 90b287f409
18 changed files with 232 additions and 244 deletions

View File

@@ -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.

View File

@@ -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">'

View File

@@ -26,3 +26,4 @@ var translations = {};
var sshKeysCache = null;
var openModalCount = 0;
var isLOTRModeActive = false;
var jailLocalWarning = false;

View File

@@ -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>'

View File

@@ -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');