diff --git a/internal/config/settings.go b/internal/config/settings.go index 88ad378..00f63c5 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -20,6 +20,7 @@ import ( "bufio" "context" "crypto/rand" + "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -55,6 +56,7 @@ type AppSettings struct { AlertCountries []string `json:"alertCountries"` SMTP SMTPSettings `json:"smtp"` CallbackURL string `json:"callbackUrl"` + CallbackSecret string `json:"callbackSecret"` AdvancedActions AdvancedActionsConfig `json:"advancedActions"` Servers []Fail2banServer `json:"servers"` @@ -136,6 +138,7 @@ const ( actionFile = "/etc/fail2ban/action.d/ui-custom-action.conf" actionCallbackPlaceholder = "__CALLBACK_URL__" actionServerIDPlaceholder = "__SERVER_ID__" + actionSecretPlaceholder = "__CALLBACK_SECRET__" ) // jailLocalBanner is the standard banner for jail.local files @@ -172,6 +175,7 @@ norestored = 1 actionban = /usr/bin/curl -X POST __CALLBACK_URL__/api/ban \ -H "Content-Type: application/json" \ + -H "X-Callback-Secret: __CALLBACK_SECRET__" \ -d "$(jq -n --arg serverId '__SERVER_ID__' \ --arg ip '' \ --arg jail '' \ @@ -388,6 +392,10 @@ func applyAppSettingsRecordLocked(rec storage.AppSettingsRecord) { currentSettings.AdvancedActions = adv } } + currentSettings.GeoIPProvider = rec.GeoIPProvider + currentSettings.GeoIPDatabasePath = rec.GeoIPDatabasePath + currentSettings.MaxLogLines = rec.MaxLogLines + currentSettings.CallbackSecret = rec.CallbackSecret } func applyServerRecordsLocked(records []storage.ServerRecord) { @@ -465,6 +473,7 @@ func toAppSettingsRecordLocked() (storage.AppSettingsRecord, error) { GeoIPProvider: currentSettings.GeoIPProvider, GeoIPDatabasePath: currentSettings.GeoIPDatabasePath, MaxLogLines: currentSettings.MaxLogLines, + CallbackSecret: currentSettings.CallbackSecret, }, nil } @@ -542,6 +551,10 @@ func setDefaultsLocked() { currentSettings.CallbackURL = fmt.Sprintf("http://127.0.0.1:%d", currentSettings.Port) } } + // Generate callback secret if not set (only generate once, never regenerate) + if currentSettings.CallbackSecret == "" { + currentSettings.CallbackSecret = generateCallbackSecret() + } if currentSettings.AlertCountries == nil { currentSettings.AlertCountries = []string{"ALL"} } @@ -667,7 +680,7 @@ func normalizeServersLocked() { hostname, _ := os.Hostname() currentSettings.Servers = []Fail2banServer{{ ID: "local", - Name: "Local Fail2ban", + Name: "Fail2ban", Type: "local", SocketPath: "/var/run/fail2ban/fail2ban.sock", LogPath: "/var/log/fail2ban.log", @@ -994,7 +1007,8 @@ func writeFail2banAction(callbackURL, serverID string) error { return fmt.Errorf("fail2ban is not installed: /etc/fail2ban/action.d directory does not exist. Please install fail2ban package first") } - actionConfig := BuildFail2banActionConfig(callbackURL, serverID) + settings := GetSettings() + actionConfig := BuildFail2banActionConfig(callbackURL, serverID, settings.CallbackSecret) err := os.WriteFile(actionFile, []byte(actionConfig), 0644) if err != nil { return fmt.Errorf("failed to write action file: %w", err) @@ -1013,7 +1027,7 @@ func cloneServer(src Fail2banServer) Fail2banServer { return dst } -func BuildFail2banActionConfig(callbackURL, serverID string) string { +func BuildFail2banActionConfig(callbackURL, serverID, secret string) string { trimmed := strings.TrimRight(strings.TrimSpace(callbackURL), "/") if trimmed == "" { trimmed = "http://127.0.0.1:8080" @@ -1021,8 +1035,43 @@ func BuildFail2banActionConfig(callbackURL, serverID string) string { if serverID == "" { serverID = "local" } + if secret == "" { + // If secret is empty, get it from settings (should be generated by setDefaultsLocked) + settings := GetSettings() + secret = settings.CallbackSecret + // Last resort: if still empty, generate one (shouldn't happen) + if secret == "" { + secret = generateCallbackSecret() + } + } config := strings.ReplaceAll(fail2banActionTemplate, actionCallbackPlaceholder, trimmed) - return strings.ReplaceAll(config, actionServerIDPlaceholder, serverID) + config = strings.ReplaceAll(config, actionServerIDPlaceholder, serverID) + config = strings.ReplaceAll(config, actionSecretPlaceholder, secret) + return config +} + +// generateCallbackSecret generates a 42-character random secret using crypto/rand. +func generateCallbackSecret() string { + // Generate 32 random bytes (256 bits of entropy) + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + // Fallback to hex encoding if crypto/rand fails (shouldn't happen) + fallbackBytes := make([]byte, 21) // 21 bytes = 42 hex chars + if _, err := rand.Read(fallbackBytes); err != nil { + // Last resort: use time-based seed (not ideal but better than nothing) + return fmt.Sprintf("%042x", time.Now().UnixNano()) + } + return hex.EncodeToString(fallbackBytes) + } + // Use base64 URL-safe encoding, which gives us 43 chars for 32 bytes + // We need exactly 42, so we'll truncate the last character (which is padding anyway) + encoded := base64.URLEncoding.EncodeToString(bytes) + // Base64 URL encoding of 32 bytes = 43 chars, take first 42 + if len(encoded) >= 42 { + return encoded[:42] + } + // If somehow shorter, pad with random hex + return encoded + hex.EncodeToString(bytes)[:42-len(encoded)] } func getCallbackURLLocked() string { diff --git a/internal/fail2ban/connector_agent.go b/internal/fail2ban/connector_agent.go index 1613471..552a062 100644 --- a/internal/fail2ban/connector_agent.go +++ b/internal/fail2ban/connector_agent.go @@ -61,9 +61,10 @@ func (ac *AgentConnector) Server() config.Fail2banServer { } func (ac *AgentConnector) ensureAction(ctx context.Context) error { + settings := config.GetSettings() payload := map[string]any{ "name": "ui-custom-action", - "config": config.BuildFail2banActionConfig(config.GetCallbackURL(), ac.server.ID), + "config": config.BuildFail2banActionConfig(config.GetCallbackURL(), ac.server.ID, settings.CallbackSecret), "callbackUrl": config.GetCallbackURL(), "setDefault": true, } diff --git a/internal/fail2ban/connector_ssh.go b/internal/fail2ban/connector_ssh.go index d5d9866..410a13c 100644 --- a/internal/fail2ban/connector_ssh.go +++ b/internal/fail2ban/connector_ssh.go @@ -244,7 +244,8 @@ func (sc *SSHConnector) FetchBanEvents(ctx context.Context, limit int) ([]BanEve func (sc *SSHConnector) ensureAction(ctx context.Context) error { callbackURL := config.GetCallbackURL() - actionConfig := config.BuildFail2banActionConfig(callbackURL, sc.server.ID) + settings := config.GetSettings() + actionConfig := config.BuildFail2banActionConfig(callbackURL, sc.server.ID, settings.CallbackSecret) payload := base64.StdEncoding.EncodeToString([]byte(actionConfig)) script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload) // Base64 encode the entire script to avoid shell escaping issues diff --git a/internal/locales/de.json b/internal/locales/de.json index 6a6805f..f1affb1 100644 --- a/internal/locales/de.json +++ b/internal/locales/de.json @@ -103,6 +103,9 @@ "settings.callback_url": "Fail2ban Callback-URL", "settings.callback_url_placeholder": "http://127.0.0.1:8080", "settings.callback_url_hint": "Diese URL wird von allen Fail2Ban-Instanzen verwendet, um die Ban-Payloads an Fail2Ban UI zu senden. Für lokale Installationen verwenden Sie denselben Port wie Fail2Ban UI (z.B. http://127.0.0.1:8080). Für Reverse-Proxy-Setups verwenden Sie falls möglich den TLS-verschlüsselten Endpunkt (z.B. https://fail2ban.example.com).", + "settings.callback_secret": "Fail2ban Callback-URL Secret", + "settings.callback_secret_placeholder": "Automatisch generiertes 42-Zeichen-Secret", + "settings.callback_secret.description": "Dieses Secret dient der Authentifizierung von Ban-API-Anfragen. Es wird automatisch in die Fail2ban-Action-Konfiguration eingefügt.", "settings.destination_email": "Ziel-E-Mail (Alarmempfänger)", "settings.destination_email_placeholder": "alerts@swissmakers.ch", "settings.alert_countries": "Alarm-Länder", diff --git a/internal/locales/de_ch.json b/internal/locales/de_ch.json index 2ce8fe5..3a5d1fd 100644 --- a/internal/locales/de_ch.json +++ b/internal/locales/de_ch.json @@ -103,6 +103,9 @@ "settings.callback_url": "Fail2ban Callback-URL", "settings.callback_url_placeholder": "http://127.0.0.1:8080", "settings.callback_url_hint": "Diä URL wird vo aune Fail2Ban-Instanze brucht, zum Ban-Payloads a Fail2Ban UI z sende. Für lokali Installatione bruchts de gliich Port wie z Fail2Ban UI (z.B. http://127.0.0.1:8080). Für Reverse-Proxy-Setups sött dr TLS-verschlüssleti Endpunkt wenn müglech brücht wärde (auso z.B. https://fail2ban.example.com).", + "settings.callback_secret": "Fail2ban Callback-URL Secret", + "settings.callback_secret_placeholder": "Automatisch generierts 42-Zeiche-Secret", + "settings.callback_secret.description": "Zur Authentifizierig vo Ban-Benachrichtigungsafroge. Es wird outomatisch id Fail2ban-Action-Konfiguration inkludiert.", "settings.destination_email": "Ziiu-Email (Alarmempfänger)", "settings.destination_email_placeholder": "alerts@swissmakers.ch", "settings.alert_countries": "Alarm-Länder", diff --git a/internal/locales/en.json b/internal/locales/en.json index 7fc541e..ed519a0 100644 --- a/internal/locales/en.json +++ b/internal/locales/en.json @@ -103,6 +103,9 @@ "settings.callback_url": "Fail2ban Callback URL", "settings.callback_url_placeholder": "http://127.0.0.1:8080", "settings.callback_url_hint": "This URL is used by all Fail2Ban instances to send ban alerts back to Fail2Ban UI. For local deployments, use the same port as Fail2Ban UI (e.g., http://127.0.0.1:8080). For reverse proxy setups, use your TLS-encrypted endpoint (e.g., https://fail2ban.example.com).", + "settings.callback_secret": "Fail2ban Callback URL Secret", + "settings.callback_secret_placeholder": "Auto-generated 42-character secret", + "settings.callback_secret.description": "This secret is used to authenticate ban API requests. It is automatically included to the fail2ban action configuration.", "settings.destination_email": "Destination Email (Alerts Receiver)", "settings.destination_email_placeholder": "alerts@swissmakers.ch", "settings.alert_countries": "Alert Countries", diff --git a/internal/locales/es.json b/internal/locales/es.json index 73cdce3..b3e3367 100644 --- a/internal/locales/es.json +++ b/internal/locales/es.json @@ -103,6 +103,9 @@ "settings.callback_url": "URL de retorno de Fail2ban", "settings.callback_url_placeholder": "http://127.0.0.1:8080", "settings.callback_url_hint": "Esta URL es utilizada por todas las instancias de Fail2Ban para enviar alertas de bloqueo a Fail2Ban UI. Para implementaciones locales, use el mismo puerto que Fail2Ban UI (ej. http://127.0.0.1:8080). Para configuraciones de proxy inverso, use su endpoint cifrado TLS (ej. https://fail2ban.example.com).", + "settings.callback_secret": "Secret de URL de Callback de Fail2ban", + "settings.callback_secret_placeholder": "Secret de 42 caracteres generado automáticamente", + "settings.callback_secret.description": "Este secret se genera automáticamente y se utiliza para autenticar las solicitudes de notificación de bloqueo. Está incluido en la configuración de acción de fail2ban.", "settings.destination_email": "Correo electrónico de destino (receptor de alertas)", "settings.destination_email_placeholder": "alerts@swissmakers.ch", "settings.alert_countries": "Países para alerta", diff --git a/internal/locales/fr.json b/internal/locales/fr.json index e4c2071..b3688a0 100644 --- a/internal/locales/fr.json +++ b/internal/locales/fr.json @@ -103,6 +103,9 @@ "settings.callback_url": "URL de rappel Fail2ban", "settings.callback_url_placeholder": "http://127.0.0.1:8080", "settings.callback_url_hint": "Cette URL est utilisée par toutes les instances Fail2Ban pour envoyer les alertes de bannissement à Fail2Ban UI. Pour les déploiements locaux, utilisez le même port que Fail2Ban UI (p. ex. http://127.0.0.1:8080). Pour les configurations de reverse proxy, utilisez votre point de terminaison chiffré TLS (p. ex. https://fail2ban.example.com).", + "settings.callback_secret": "Secret d'URL de Callback Fail2ban", + "settings.callback_secret_placeholder": "Secret de 42 caractères généré automatiquement", + "settings.callback_secret.description": "Ce secret est généré automatiquement et utilisé pour authentifier les demandes de notification de bannissement. Il est inclus dans la configuration d'action de fail2ban.", "settings.destination_email": "Email de destination (récepteur des alertes)", "settings.destination_email_placeholder": "alerts@swissmakers.ch", "settings.alert_countries": "Pays d'alerte", diff --git a/internal/locales/it.json b/internal/locales/it.json index cbbe16a..b72fc76 100644 --- a/internal/locales/it.json +++ b/internal/locales/it.json @@ -103,6 +103,9 @@ "settings.callback_url": "URL di callback Fail2ban", "settings.callback_url_placeholder": "http://127.0.0.1:8080", "settings.callback_url_hint": "Questo URL viene utilizzato da tutte le istanze Fail2Ban per inviare gli avvisi di ban a Fail2Ban UI. Per le distribuzioni locali, utilizzare la stessa porta di Fail2Ban UI (es. http://127.0.0.1:8080). Per le configurazioni di reverse proxy, utilizzare il proprio endpoint crittografato TLS (es. https://fail2ban.example.com).", + "settings.callback_secret": "Secret URL di Callback Fail2ban", + "settings.callback_secret_placeholder": "Secret di 42 caratteri generato automaticamente", + "settings.callback_secret.description": "Questo secret viene generato automaticamente e utilizzato per autenticare le richieste di notifica di ban. È incluso nella configurazione dell'azione di fail2ban.", "settings.destination_email": "Email di destinazione (ricevente allarmi)", "settings.destination_email_placeholder": "alerts@swissmakers.ch", "settings.alert_countries": "Paesi per allarme", diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 0fafa50..dd4b012 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -73,6 +73,7 @@ type AppSettingsRecord struct { GeoIPProvider string GeoIPDatabasePath string MaxLogLines int + CallbackSecret string } type ServerRecord struct { @@ -174,17 +175,17 @@ func GetAppSettings(ctx context.Context) (AppSettingsRecord, bool, error) { } row := db.QueryRowContext(ctx, ` -SELECT language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines +SELECT language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, callback_secret FROM app_settings WHERE id = 1`) var ( - lang, callback, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath sql.NullString - port, smtpPort, maxretry, maxLogLines sql.NullInt64 - debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn sql.NullInt64 + lang, callback, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath, callbackSecret sql.NullString + port, smtpPort, maxretry, maxLogLines sql.NullInt64 + debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn sql.NullInt64 ) - err := row.Scan(&lang, &port, &debug, &callback, &restartNeeded, &alerts, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines) + err := row.Scan(&lang, &port, &debug, &callback, &restartNeeded, &alerts, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines, &callbackSecret) if errors.Is(err, sql.ErrNoRows) { return AppSettingsRecord{}, false, nil } @@ -218,6 +219,7 @@ WHERE id = 1`) GeoIPProvider: stringFromNull(geoipProvider), GeoIPDatabasePath: stringFromNull(geoipDatabasePath), MaxLogLines: intFromNull(maxLogLines), + CallbackSecret: stringFromNull(callbackSecret), } return rec, true, nil @@ -229,9 +231,9 @@ func SaveAppSettings(ctx context.Context, rec AppSettingsRecord) error { } _, err := db.ExecContext(ctx, ` INSERT INTO app_settings ( - id, language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines + id, language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, callback_secret ) VALUES ( - 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ON CONFLICT(id) DO UPDATE SET language = excluded.language, port = excluded.port, @@ -257,7 +259,8 @@ INSERT INTO app_settings ( advanced_actions = excluded.advanced_actions, geoip_provider = excluded.geoip_provider, geoip_database_path = excluded.geoip_database_path, - max_log_lines = excluded.max_log_lines + max_log_lines = excluded.max_log_lines, + callback_secret = excluded.callback_secret `, rec.Language, rec.Port, boolToInt(rec.Debug), @@ -283,6 +286,7 @@ INSERT INTO app_settings ( rec.GeoIPProvider, rec.GeoIPDatabasePath, rec.MaxLogLines, + rec.CallbackSecret, ) return err } @@ -806,7 +810,8 @@ CREATE TABLE IF NOT EXISTS app_settings ( advanced_actions TEXT, geoip_provider TEXT, geoip_database_path TEXT, - max_log_lines INTEGER + max_log_lines INTEGER, + callback_secret TEXT ); CREATE TABLE IF NOT EXISTS servers ( @@ -914,6 +919,13 @@ CREATE INDEX IF NOT EXISTS idx_perm_blocks_status ON permanent_blocks(status); } } + // Add callback_secret column + if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN callback_secret TEXT`); err != nil { + if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") { + return err + } + } + // Set default values for new columns if they are NULL if _, err := db.ExecContext(ctx, `UPDATE app_settings SET geoip_provider = 'maxmind' WHERE geoip_provider IS NULL`); err != nil { log.Printf("Warning: Failed to set default value for geoip_provider: %v", err) diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index af76958..fe0c623 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -19,6 +19,7 @@ package web import ( "bytes" "context" + "crypto/subtle" "crypto/tls" "encoding/json" "errors" @@ -171,6 +172,31 @@ func UnbanIPHandler(c *gin.Context) { // BanNotificationHandler processes incoming ban notifications from Fail2Ban. func BanNotificationHandler(c *gin.Context) { + // Validate callback secret + settings := config.GetSettings() + providedSecret := c.GetHeader("X-Callback-Secret") + expectedSecret := settings.CallbackSecret + + // Use constant-time comparison to prevent timing attacks + if expectedSecret == "" { + log.Printf("⚠️ Callback secret not configured, rejecting request from %s", c.ClientIP()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Callback secret not configured"}) + return + } + + if providedSecret == "" { + log.Printf("⚠️ Missing X-Callback-Secret header in request from %s", c.ClientIP()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing X-Callback-Secret header"}) + return + } + + // Constant-time comparison + if subtle.ConstantTimeCompare([]byte(providedSecret), []byte(expectedSecret)) != 1 { + log.Printf("⚠️ Invalid callback secret in request from %s", c.ClientIP()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid callback secret"}) + return + } + var request struct { ServerID string `json:"serverId"` IP string `json:"ip" binding:"required"` diff --git a/pkg/web/static/js/core.js b/pkg/web/static/js/core.js index 8f80cba..bc40555 100644 --- a/pkg/web/static/js/core.js +++ b/pkg/web/static/js/core.js @@ -60,7 +60,7 @@ function showBanEventToast(event) { + ' ' + ' ' + '
' - + '
New Block Detected
' + + '
New block occurred
' + '
' + ' ' + escapeHtml(ip) + '' + ' banned in ' diff --git a/pkg/web/static/js/settings.js b/pkg/web/static/js/settings.js index 2d56d87..0026b3a 100644 --- a/pkg/web/static/js/settings.js +++ b/pkg/web/static/js/settings.js @@ -48,6 +48,19 @@ function loadSettings() { // Set callback URL and add auto-update listener for port changes const callbackURLInput = document.getElementById('callbackURL'); callbackURLInput.value = data.callbackUrl || ''; + const callbackSecretInput = document.getElementById('callbackSecret'); + const toggleLink = document.getElementById('toggleCallbackSecretLink'); + if (callbackSecretInput) { + callbackSecretInput.value = data.callbackSecret || ''; + // Reset to password type when loading + if (callbackSecretInput.type === 'text') { + callbackSecretInput.type = 'password'; + } + // Update link text + if (toggleLink) { + toggleLink.textContent = 'show secret'; + } + } // Auto-update callback URL when port changes (if using default localhost pattern) function updateCallbackURLIfDefault() { @@ -159,6 +172,7 @@ function saveSettings(event) { debug: document.getElementById('debugMode').checked, destemail: document.getElementById('destEmail').value.trim(), callbackUrl: callbackUrl, + callbackSecret: document.getElementById('callbackSecret').value.trim(), alertCountries: selectedCountries.length > 0 ? selectedCountries : ["ALL"], bantimeIncrement: document.getElementById('bantimeIncrement').checked, defaultJailEnable: document.getElementById('defaultJailEnable').checked, @@ -428,3 +442,15 @@ if (advancedIntegrationSelect) { advancedIntegrationSelect.addEventListener('change', updateAdvancedIntegrationFields); } +// Toggle callback secret visibility +function toggleCallbackSecretVisibility() { + const input = document.getElementById('callbackSecret'); + const link = document.getElementById('toggleCallbackSecretLink'); + + if (!input || !link) return; + + const isPassword = input.type === 'password'; + input.type = isPassword ? 'text' : 'password'; + link.textContent = isPassword ? 'hide secret' : 'show secret'; +} + diff --git a/pkg/web/templates/index.html b/pkg/web/templates/index.html index dd3280b..f1dfb52 100644 --- a/pkg/web/templates/index.html +++ b/pkg/web/templates/index.html @@ -220,6 +220,16 @@

This URL is used by all Fail2Ban instances to send ban alerts back to Fail2Ban UI. For local deployments, use the same port as Fail2Ban UI (e.g., http://127.0.0.1:8080). For reverse proxy setups, use your TLS-encrypted endpoint (e.g., https://fail2ban.example.com).

+
+
+ + show secret +
+ +

This secret is automatically generated and used to authenticate ban notification requests. It is included in the fail2ban action configuration.

+
+