mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Implement X-Callback-Secret for validating API requests
This commit is contained in:
@@ -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 '<ip>' \
|
||||
--arg jail '<name>' \
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user