mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Fix ssh connector and rewrite the get jail function, to get all in only one ssh-connection, instead of one for every jail (speed-up), add missing translations
This commit is contained in:
@@ -69,7 +69,10 @@ PY`
|
||||
|
||||
// SSHConnector connects to a remote Fail2ban instance over SSH.
|
||||
type SSHConnector struct {
|
||||
server config.Fail2banServer
|
||||
server config.Fail2banServer
|
||||
fail2banPath string // Cache the fail2ban path
|
||||
pathCached bool // Track if path is cached
|
||||
pathMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewSSHConnector creates a new SSH connector.
|
||||
@@ -444,37 +447,43 @@ func (sc *SSHConnector) buildSSHArgs(command []string) []string {
|
||||
}
|
||||
|
||||
// listRemoteFiles lists files in a remote directory matching a pattern.
|
||||
// Uses Python to list files, which works better with FACL permissions than find/ls.
|
||||
// Uses find command which works reliably with FACL permissions.
|
||||
func (sc *SSHConnector) listRemoteFiles(ctx context.Context, directory, pattern string) ([]string, error) {
|
||||
// Use Python to list files - works better with FACL permissions
|
||||
script := fmt.Sprintf(`python3 -c "
|
||||
import os
|
||||
import sys
|
||||
directory = %q
|
||||
pattern = %q
|
||||
try:
|
||||
if os.path.isdir(directory):
|
||||
files = os.listdir(directory)
|
||||
for f in files:
|
||||
if f.endswith(pattern) and not f.startswith('.'):
|
||||
full_path = os.path.join(directory, f)
|
||||
if os.path.isfile(full_path):
|
||||
print(full_path)
|
||||
except Exception as e:
|
||||
sys.stderr.write(f'Error listing files: {e}\\n')
|
||||
sys.exit(1)
|
||||
"`, directory, pattern)
|
||||
// Use find command with absolute path - it will handle non-existent directories gracefully
|
||||
// Find files ending with pattern, exclude hidden files, and ensure they're regular files
|
||||
// Redirect stderr to /dev/null to suppress "No such file or directory" errors
|
||||
// Pass the entire command as a single string to SSH (SSH executes through a shell by default)
|
||||
cmd := fmt.Sprintf(`find "%s" -maxdepth 1 -type f -name "*%s" ! -name ".*" 2>/dev/null | sort`, directory, pattern)
|
||||
|
||||
out, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", script})
|
||||
out, err := sc.runRemoteCommand(ctx, []string{cmd})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list files in %s: %w", directory, err)
|
||||
// If find fails (e.g., directory doesn't exist or permission denied), return empty list (not an error)
|
||||
config.DebugLog("Find command failed for %s on server %s: %v, returning empty list", directory, sc.server.Name, err)
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// If find succeeds but directory doesn't exist, it will return empty output
|
||||
// This is fine - we'll just return an empty list
|
||||
|
||||
var files []string
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
files = append(files, line)
|
||||
// Skip empty lines, current directory marker, and relative paths
|
||||
if line == "" || line == "." || strings.HasPrefix(line, "./") {
|
||||
continue
|
||||
}
|
||||
// Only process files that match our pattern (end with .local or .conf)
|
||||
// and are actually in the target directory
|
||||
if strings.HasSuffix(line, pattern) {
|
||||
// If it's already an absolute path starting with our directory, use it directly
|
||||
if strings.HasPrefix(line, directory) {
|
||||
files = append(files, line)
|
||||
} else if !strings.HasPrefix(line, "/") {
|
||||
// Relative path, join with directory
|
||||
fullPath := filepath.Join(directory, line)
|
||||
files = append(files, fullPath)
|
||||
}
|
||||
// Skip any other absolute paths that don't start with our directory
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +510,7 @@ func (sc *SSHConnector) writeRemoteFile(ctx context.Context, filePath, content s
|
||||
REMOTEEOF
|
||||
`, filePath, escaped)
|
||||
|
||||
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", script})
|
||||
_, err := sc.runRemoteCommand(ctx, []string{script})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write remote file %s: %w", filePath, err)
|
||||
}
|
||||
@@ -526,7 +535,7 @@ func (sc *SSHConnector) ensureRemoteLocalFile(ctx context.Context, basePath, nam
|
||||
fi
|
||||
`, localPath, confPath, confPath, localPath, localPath)
|
||||
|
||||
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", script})
|
||||
_, err := sc.runRemoteCommand(ctx, []string{script})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to ensure remote .local file %s: %w", localPath, err)
|
||||
}
|
||||
@@ -535,22 +544,46 @@ func (sc *SSHConnector) ensureRemoteLocalFile(ctx context.Context, basePath, nam
|
||||
|
||||
// getFail2banPath detects the fail2ban configuration path on the remote system.
|
||||
// Returns /config/fail2ban for linuxserver images, or /etc/fail2ban for standard installations.
|
||||
// Uses caching to avoid repeated SSH calls.
|
||||
func (sc *SSHConnector) getFail2banPath(ctx context.Context) string {
|
||||
// Check if /config/fail2ban exists (linuxserver image)
|
||||
checkScript := `if [ -d "/config/fail2ban" ]; then echo "/config/fail2ban"; elif [ -d "/etc/fail2ban" ]; then echo "/etc/fail2ban"; else echo "/etc/fail2ban"; fi`
|
||||
out, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", checkScript})
|
||||
// Try to read from cache first
|
||||
sc.pathMutex.RLock()
|
||||
if sc.pathCached {
|
||||
path := sc.fail2banPath
|
||||
sc.pathMutex.RUnlock()
|
||||
return path
|
||||
}
|
||||
sc.pathMutex.RUnlock()
|
||||
|
||||
// Acquire write lock to update cache
|
||||
sc.pathMutex.Lock()
|
||||
defer sc.pathMutex.Unlock()
|
||||
|
||||
// Double-check after acquiring write lock (another goroutine might have cached it)
|
||||
if sc.pathCached {
|
||||
return sc.fail2banPath
|
||||
}
|
||||
|
||||
// Actually fetch the path
|
||||
checkCmd := `test -d "/config/fail2ban" && echo "/config/fail2ban" || (test -d "/etc/fail2ban" && echo "/etc/fail2ban" || echo "/etc/fail2ban")`
|
||||
out, err := sc.runRemoteCommand(ctx, []string{checkCmd})
|
||||
if err == nil {
|
||||
path := strings.TrimSpace(out)
|
||||
if path != "" {
|
||||
sc.fail2banPath = path
|
||||
sc.pathCached = true
|
||||
return path
|
||||
}
|
||||
}
|
||||
// Default to /etc/fail2ban
|
||||
return "/etc/fail2ban"
|
||||
sc.fail2banPath = "/etc/fail2ban"
|
||||
sc.pathCached = true
|
||||
return sc.fail2banPath
|
||||
}
|
||||
|
||||
// GetAllJails implements Connector.
|
||||
// Discovers all jails from filesystem (mirrors local connector behavior).
|
||||
// Optimized to read all files in a single SSH command instead of individual reads.
|
||||
func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
|
||||
fail2banPath := sc.getFail2banPath(ctx)
|
||||
jailDPath := filepath.Join(fail2banPath, "jail.d")
|
||||
@@ -559,23 +592,167 @@ func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
|
||||
processedFiles := make(map[string]bool) // Track base names to avoid duplicates
|
||||
processedJails := make(map[string]bool) // Track jail names to avoid duplicates
|
||||
|
||||
// Use a Python script to read all files in a single SSH command
|
||||
// This is much more efficient than reading each file individually
|
||||
readAllScript := fmt.Sprintf(`python3 << 'PYEOF'
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
jail_d_path = %q
|
||||
files_data = {}
|
||||
|
||||
# Read all .local files first
|
||||
local_files = []
|
||||
if os.path.isdir(jail_d_path):
|
||||
for filename in os.listdir(jail_d_path):
|
||||
if filename.endswith('.local') and not filename.startswith('.'):
|
||||
local_files.append(os.path.join(jail_d_path, filename))
|
||||
|
||||
# Process .local files
|
||||
for filepath in sorted(local_files):
|
||||
try:
|
||||
filename = os.path.basename(filepath)
|
||||
basename = filename[:-6] # Remove .local
|
||||
if basename and basename not in files_data:
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
files_data[basename] = {'path': filepath, 'content': content, 'type': 'local'}
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"Error reading {filepath}: {e}\n")
|
||||
|
||||
# Read all .conf files that don't have corresponding .local files
|
||||
conf_files = []
|
||||
if os.path.isdir(jail_d_path):
|
||||
for filename in os.listdir(jail_d_path):
|
||||
if filename.endswith('.conf') and not filename.startswith('.'):
|
||||
basename = filename[:-5] # Remove .conf
|
||||
if basename not in files_data:
|
||||
conf_files.append(os.path.join(jail_d_path, filename))
|
||||
|
||||
# Process .conf files
|
||||
for filepath in sorted(conf_files):
|
||||
try:
|
||||
filename = os.path.basename(filepath)
|
||||
basename = filename[:-5] # Remove .conf
|
||||
if basename:
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
files_data[basename] = {'path': filepath, 'content': content, 'type': 'conf'}
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"Error reading {filepath}: {e}\n")
|
||||
|
||||
# Output files with a delimiter: FILE_START:path:type\ncontent\nFILE_END\n
|
||||
for basename, data in sorted(files_data.items()):
|
||||
print(f"FILE_START:{data['path']}:{data['type']}")
|
||||
print(data['content'], end='')
|
||||
print("FILE_END")
|
||||
PYEOF`, jailDPath)
|
||||
|
||||
output, err := sc.runRemoteCommand(ctx, []string{readAllScript})
|
||||
if err != nil {
|
||||
// Fallback to individual file reads if the script fails
|
||||
config.DebugLog("Failed to read all jail files at once on server %s, falling back to individual reads: %v", sc.server.Name, err)
|
||||
return sc.getAllJailsFallback(ctx, jailDPath)
|
||||
}
|
||||
|
||||
// Parse the output: files are separated by FILE_START:path:type\ncontent\nFILE_END\n
|
||||
var currentFile string
|
||||
var currentContent strings.Builder
|
||||
var currentType string
|
||||
inFile := false
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "FILE_START:") {
|
||||
// Save previous file if any
|
||||
if inFile && currentFile != "" {
|
||||
content := currentContent.String()
|
||||
jails := parseJailConfigContent(content)
|
||||
for _, jail := range jails {
|
||||
if jail.JailName != "" && jail.JailName != "DEFAULT" && !processedJails[jail.JailName] {
|
||||
allJails = append(allJails, jail)
|
||||
processedJails[jail.JailName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parse new file header: FILE_START:path:type
|
||||
parts := strings.SplitN(line, ":", 3)
|
||||
if len(parts) == 3 {
|
||||
currentFile = parts[1]
|
||||
currentType = parts[2]
|
||||
currentContent.Reset()
|
||||
inFile = true
|
||||
filename := filepath.Base(currentFile)
|
||||
var baseName string
|
||||
if currentType == "local" {
|
||||
baseName = strings.TrimSuffix(filename, ".local")
|
||||
} else {
|
||||
baseName = strings.TrimSuffix(filename, ".conf")
|
||||
}
|
||||
if baseName != "" {
|
||||
processedFiles[baseName] = true
|
||||
}
|
||||
}
|
||||
} else if line == "FILE_END" {
|
||||
// End of file, process it
|
||||
if inFile && currentFile != "" {
|
||||
content := currentContent.String()
|
||||
jails := parseJailConfigContent(content)
|
||||
for _, jail := range jails {
|
||||
if jail.JailName != "" && jail.JailName != "DEFAULT" && !processedJails[jail.JailName] {
|
||||
allJails = append(allJails, jail)
|
||||
processedJails[jail.JailName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
inFile = false
|
||||
currentFile = ""
|
||||
currentContent.Reset()
|
||||
} else if inFile {
|
||||
// Content line
|
||||
if currentContent.Len() > 0 {
|
||||
currentContent.WriteString("\n")
|
||||
}
|
||||
currentContent.WriteString(line)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle last file if output doesn't end with FILE_END
|
||||
if inFile && currentFile != "" {
|
||||
content := currentContent.String()
|
||||
jails := parseJailConfigContent(content)
|
||||
for _, jail := range jails {
|
||||
if jail.JailName != "" && jail.JailName != "DEFAULT" && !processedJails[jail.JailName] {
|
||||
allJails = append(allJails, jail)
|
||||
processedJails[jail.JailName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allJails, nil
|
||||
}
|
||||
|
||||
// getAllJailsFallback is the fallback method that reads files individually.
|
||||
// Used when the optimized batch read fails.
|
||||
func (sc *SSHConnector) getAllJailsFallback(ctx context.Context, jailDPath string) ([]JailInfo, error) {
|
||||
var allJails []JailInfo
|
||||
processedFiles := make(map[string]bool)
|
||||
processedJails := make(map[string]bool)
|
||||
|
||||
// List all .local files first
|
||||
localFiles, err := sc.listRemoteFiles(ctx, jailDPath, ".local")
|
||||
if err != nil {
|
||||
config.DebugLog("Failed to list .local files in jail.d on server %s: %v", sc.server.Name, err)
|
||||
// Continue with .conf files
|
||||
} else {
|
||||
// Process .local files
|
||||
for _, filePath := range localFiles {
|
||||
filename := filepath.Base(filePath)
|
||||
baseName := strings.TrimSuffix(filename, ".local")
|
||||
if baseName == "" || processedFiles[baseName] {
|
||||
continue
|
||||
}
|
||||
|
||||
processedFiles[baseName] = true
|
||||
|
||||
// Read and parse the file
|
||||
content, err := sc.readRemoteFile(ctx, filePath)
|
||||
if err != nil {
|
||||
config.DebugLog("Failed to read jail file %s on server %s: %v", filePath, sc.server.Name, err)
|
||||
@@ -597,17 +774,14 @@ func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
|
||||
if err != nil {
|
||||
config.DebugLog("Failed to list .conf files in jail.d on server %s: %v", sc.server.Name, err)
|
||||
} else {
|
||||
// Process .conf files
|
||||
for _, filePath := range confFiles {
|
||||
filename := filepath.Base(filePath)
|
||||
baseName := strings.TrimSuffix(filename, ".conf")
|
||||
if baseName == "" || processedFiles[baseName] {
|
||||
continue
|
||||
}
|
||||
|
||||
processedFiles[baseName] = true
|
||||
|
||||
// Read and parse the file
|
||||
content, err := sc.readRemoteFile(ctx, filePath)
|
||||
if err != nil {
|
||||
config.DebugLog("Failed to read jail file %s on server %s: %v", filePath, sc.server.Name, err)
|
||||
@@ -632,12 +806,6 @@ func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map
|
||||
fail2banPath := sc.getFail2banPath(ctx)
|
||||
jailDPath := filepath.Join(fail2banPath, "jail.d")
|
||||
|
||||
// Ensure jail.d directory exists
|
||||
_, err := sc.runRemoteCommand(ctx, []string{"mkdir", "-p", jailDPath})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create jail.d directory: %w", err)
|
||||
}
|
||||
|
||||
// Update each jail in its own .local file
|
||||
for jailName, enabled := range updates {
|
||||
// Validate jail name - skip empty or invalid names
|
||||
@@ -650,8 +818,9 @@ func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map
|
||||
localPath := filepath.Join(jailDPath, jailName+".local")
|
||||
confPath := filepath.Join(jailDPath, jailName+".conf")
|
||||
|
||||
// Ensure .local file exists (copy from .conf if needed)
|
||||
ensureScript := fmt.Sprintf(`
|
||||
// Combined script: ensure .local file exists AND read it in one SSH call
|
||||
// This reduces SSH round-trips from 2 to 1 per jail
|
||||
combinedScript := fmt.Sprintf(`
|
||||
if [ ! -f "%s" ]; then
|
||||
if [ -f "%s" ]; then
|
||||
cp "%s" "%s"
|
||||
@@ -659,16 +828,12 @@ func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map
|
||||
echo "[%s]" > "%s"
|
||||
fi
|
||||
fi
|
||||
`, localPath, confPath, confPath, localPath, jailName, localPath)
|
||||
cat "%s"
|
||||
`, localPath, confPath, confPath, localPath, jailName, localPath, localPath)
|
||||
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", ensureScript}); err != nil {
|
||||
return fmt.Errorf("failed to ensure .local file for jail %s: %w", jailName, err)
|
||||
}
|
||||
|
||||
// Read existing .local file
|
||||
content, err := sc.runRemoteCommand(ctx, []string{"cat", localPath})
|
||||
content, err := sc.runRemoteCommand(ctx, []string{combinedScript})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read jail .local file %s: %w", localPath, err)
|
||||
return fmt.Errorf("failed to ensure and read .local file for jail %s: %w", jailName, err)
|
||||
}
|
||||
|
||||
// Update enabled state in existing file
|
||||
@@ -717,7 +882,7 @@ func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map
|
||||
// Write updated content to .local file
|
||||
newContent := strings.Join(outputLines, "\n")
|
||||
cmd := fmt.Sprintf("cat <<'EOF' | tee %s >/dev/null\n%s\nEOF", localPath, newContent)
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd}); err != nil {
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{cmd}); err != nil {
|
||||
return fmt.Errorf("failed to write jail .local file %s: %w", localPath, err)
|
||||
}
|
||||
}
|
||||
@@ -810,9 +975,11 @@ func (sc *SSHConnector) TestFilter(ctx context.Context, filterName string, logLi
|
||||
filterName = strings.ReplaceAll(filterName, "/", "")
|
||||
filterName = strings.ReplaceAll(filterName, "..", "")
|
||||
|
||||
// Get the fail2ban path dynamically
|
||||
fail2banPath := sc.getFail2banPath(ctx)
|
||||
// Try .local first, then fallback to .conf
|
||||
localPath := fmt.Sprintf("/etc/fail2ban/filter.d/%s.local", filterName)
|
||||
confPath := fmt.Sprintf("/etc/fail2ban/filter.d/%s.conf", filterName)
|
||||
localPath := filepath.Join(fail2banPath, "filter.d", filterName+".local")
|
||||
confPath := filepath.Join(fail2banPath, "filter.d", filterName+".conf")
|
||||
|
||||
const heredocMarker = "F2B_FILTER_TEST_LOG"
|
||||
logContent := strings.Join(cleaned, "\n")
|
||||
@@ -839,7 +1006,7 @@ cat <<'%[3]s' > "$TMPFILE"
|
||||
fail2ban-regex "$TMPFILE" "$FILTER_PATH" || true
|
||||
`, localPath, confPath, heredocMarker, logContent)
|
||||
|
||||
out, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", script})
|
||||
out, err := sc.runRemoteCommand(ctx, []string{script})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -965,7 +1132,7 @@ fi
|
||||
`, logpath)
|
||||
}
|
||||
|
||||
out, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", script})
|
||||
out, err := sc.runRemoteCommand(ctx, []string{script})
|
||||
if err != nil {
|
||||
return []string{}, nil // Return empty on error
|
||||
}
|
||||
@@ -1099,7 +1266,7 @@ PYEOF
|
||||
`, originalPath)
|
||||
|
||||
// Run resolution script
|
||||
resolveOut, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", resolveScript})
|
||||
resolveOut, err := sc.runRemoteCommand(ctx, []string{resolveScript})
|
||||
if err != nil {
|
||||
return originalPath, "", nil, fmt.Errorf("failed to resolve variables: %w", err)
|
||||
}
|
||||
@@ -1139,7 +1306,7 @@ func (sc *SSHConnector) UpdateDefaultSettings(ctx context.Context, settings conf
|
||||
if existingContent != "" {
|
||||
// Use sed to remove lines starting with # (but preserve empty lines)
|
||||
removeCommentsCmd := fmt.Sprintf("sed '/^[[:space:]]*#/d' %s", jailLocalPath)
|
||||
uncommentedContent, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", removeCommentsCmd})
|
||||
uncommentedContent, err := sc.runRemoteCommand(ctx, []string{removeCommentsCmd})
|
||||
if err == nil {
|
||||
existingContent = uncommentedContent
|
||||
}
|
||||
@@ -1182,7 +1349,7 @@ func (sc *SSHConnector) UpdateDefaultSettings(ctx context.Context, settings conf
|
||||
defaultLines = append(defaultLines, "")
|
||||
newContent := strings.Join(defaultLines, "\n")
|
||||
cmd := fmt.Sprintf("cat <<'EOF' | tee %s >/dev/null\n%s\nEOF", jailLocalPath, newContent)
|
||||
_, err = sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
|
||||
_, err = sc.runRemoteCommand(ctx, []string{cmd})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1285,7 +1452,7 @@ with open(jail_file, 'w') as f:
|
||||
f.writelines(output_lines)
|
||||
PY`, escapeForShell(jailLocalPath), escapeForShell(ignoreIPStr), escapeForShell(banactionVal), escapeForShell(banactionAllportsVal), settings.BantimeIncrement, settings.DefaultJailEnable, escapeForShell(settings.Bantime), escapeForShell(settings.Findtime), settings.Maxretry, escapeForShell(settings.Destemail))
|
||||
|
||||
_, err = sc.runRemoteCommand(ctx, []string{"bash", "-lc", updateScript})
|
||||
_, err = sc.runRemoteCommand(ctx, []string{updateScript})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1370,7 +1537,7 @@ action = %(action_mwlg)s
|
||||
JAILLOCAL
|
||||
`, jailLocalPath, escaped)
|
||||
|
||||
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", writeScript})
|
||||
_, err := sc.runRemoteCommand(ctx, []string{writeScript})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1381,7 +1548,7 @@ func (sc *SSHConnector) MigrateJailsFromJailLocalRemote(ctx context.Context) err
|
||||
|
||||
// Check if jail.local exists
|
||||
checkScript := fmt.Sprintf("test -f %s && echo 'exists' || echo 'notfound'", jailLocalPath)
|
||||
out, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", checkScript})
|
||||
out, err := sc.runRemoteCommand(ctx, []string{checkScript})
|
||||
if err != nil || strings.TrimSpace(out) != "exists" {
|
||||
config.DebugLog("No jails to migrate from jail.local on server %s (file does not exist)", sc.server.Name)
|
||||
return nil // Nothing to migrate
|
||||
@@ -1408,14 +1575,14 @@ func (sc *SSHConnector) MigrateJailsFromJailLocalRemote(ctx context.Context) err
|
||||
// Create backup
|
||||
backupPath := jailLocalPath + ".backup." + fmt.Sprintf("%d", time.Now().Unix())
|
||||
backupScript := fmt.Sprintf("cp %s %s", jailLocalPath, backupPath)
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", backupScript}); err != nil {
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{backupScript}); err != nil {
|
||||
return fmt.Errorf("failed to create backup on server %s: %w", sc.server.Name, err)
|
||||
}
|
||||
config.DebugLog("Created backup of jail.local at %s on server %s", backupPath, sc.server.Name)
|
||||
|
||||
// Ensure jail.d directory exists
|
||||
ensureDirScript := fmt.Sprintf("mkdir -p %s", jailDPath)
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", ensureDirScript}); err != nil {
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{ensureDirScript}); err != nil {
|
||||
return fmt.Errorf("failed to create jail.d directory on server %s: %w", sc.server.Name, err)
|
||||
}
|
||||
|
||||
@@ -1430,7 +1597,7 @@ func (sc *SSHConnector) MigrateJailsFromJailLocalRemote(ctx context.Context) err
|
||||
|
||||
// Check if .local file already exists
|
||||
checkFileScript := fmt.Sprintf("test -f %s && echo 'exists' || echo 'notfound'", jailFilePath)
|
||||
fileOut, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", checkFileScript})
|
||||
fileOut, err := sc.runRemoteCommand(ctx, []string{checkFileScript})
|
||||
if err == nil && strings.TrimSpace(fileOut) == "exists" {
|
||||
config.DebugLog("Skipping migration for jail %s on server %s: .local file already exists", jailName, sc.server.Name)
|
||||
continue
|
||||
@@ -1443,7 +1610,7 @@ func (sc *SSHConnector) MigrateJailsFromJailLocalRemote(ctx context.Context) err
|
||||
%s
|
||||
JAILEOF
|
||||
'`, jailFilePath, escapedContent)
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", writeScript}); err != nil {
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{writeScript}); err != nil {
|
||||
return fmt.Errorf("failed to write jail file %s: %w", jailFilePath, err)
|
||||
}
|
||||
config.DebugLog("Migrated jail %s to %s on server %s", jailName, jailFilePath, sc.server.Name)
|
||||
@@ -1459,7 +1626,7 @@ JAILEOF
|
||||
%s
|
||||
LOCALEOF
|
||||
'`, jailLocalPath, escapedDefault)
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", writeLocalScript}); err != nil {
|
||||
if _, err := sc.runRemoteCommand(ctx, []string{writeLocalScript}); err != nil {
|
||||
return fmt.Errorf("failed to rewrite jail.local: %w", err)
|
||||
}
|
||||
config.DebugLog("Migration completed on server %s: moved %d jails to jail.d/", sc.server.Name, migratedCount)
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Alarm-Länder",
|
||||
"settings.alert_countries_description": "Wählen Sie die Länder aus, für die E-Mail-Alarme ausgelöst werden sollen, wenn eine Sperrung erfolgt.",
|
||||
"settings.email_alerts": "E-Mail-Benachrichtigungseinstellungen",
|
||||
"settings.email_alerts_for_bans": "E-Mail-Benachrichtigungen für Sperrungen aktivieren",
|
||||
"settings.email_alerts_for_unbans": "E-Mail-Benachrichtigungen für Entsperrungen aktivieren",
|
||||
"settings.smtp": "SMTP-Konfiguration",
|
||||
@@ -198,6 +199,23 @@
|
||||
"settings.save": "Speichern",
|
||||
"modal.filter_config": "Filter / Jail-Konfiguration:",
|
||||
"modal.filter_config_edit": "Filter / Jail bearbeiten",
|
||||
"modal.filter_config_label": "Filter-Konfiguration",
|
||||
"modal.filter_config_hint": "Wenn leer gelassen, wird eine leere Filterdatei erstellt.",
|
||||
"modal.filter_name": "Filter-Name",
|
||||
"modal.filter_name_hint": "Nur alphanumerische Zeichen, Bindestriche und Unterstriche sind erlaubt.",
|
||||
"modal.jail_config": "Jail-Konfiguration",
|
||||
"modal.jail_config_hint": "Die Jail-Konfiguration wird automatisch ausgefüllt, wenn Sie einen Filter auswählen.",
|
||||
"modal.jail_config_label": "Jail-Konfiguration",
|
||||
"modal.jail_filter": "Filter (optional)",
|
||||
"modal.jail_filter_hint": "Die Auswahl eines Filters füllt die Jail-Konfiguration automatisch aus.",
|
||||
"modal.jail_name": "Jail-Name",
|
||||
"modal.jail_name_hint": "Nur alphanumerische Zeichen, Bindestriche und Unterstriche sind erlaubt.",
|
||||
"modal.test_logpath": "Logpfad testen",
|
||||
"modal.create": "Erstellen",
|
||||
"modal.create_filter": "Neuen Filter erstellen",
|
||||
"modal.create_filter_title": "Neuen Filter erstellen",
|
||||
"modal.create_jail": "Neues Jail erstellen",
|
||||
"modal.create_jail_title": "Neues Jail erstellen",
|
||||
"modal.cancel": "Abbrechen",
|
||||
"modal.save": "Speichern",
|
||||
"modal.close": "Schließen",
|
||||
@@ -229,9 +247,10 @@
|
||||
"servers.form.hostname": "Server-Hostname",
|
||||
"servers.form.hostname_placeholder": "optional",
|
||||
"servers.form.ssh_user": "SSH-Benutzer",
|
||||
"servers.form.ssh_user_placeholder": "root",
|
||||
"servers.form.ssh_user_placeholder": "sa_fail2ban",
|
||||
"servers.form.ssh_key": "Pfad zum SSH-Schlüssel",
|
||||
"servers.form.ssh_key_placeholder": "~/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_placeholder": "/config/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_help": "Platzieren Sie Ihren SSH-Private-Key im Verzeichnis /config/.ssh/ (gemountetes Config-Volume). Die Schlüsseldatei muss die Berechtigungen 600 haben (chmod 600). Beispiel: /config/.ssh/id_rsa",
|
||||
"servers.form.agent_url": "Agent-URL",
|
||||
"servers.form.agent_url_placeholder": "https://host:9443",
|
||||
"servers.form.agent_secret": "Agent-Secret",
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Alarm-Länder",
|
||||
"settings.alert_countries_description": "Wähl d'Länder us, für weli du per Email ä Alarm becho wetsch, wenn e Sperrig erfolgt.",
|
||||
"settings.email_alerts": "Email-Benachrichtigungsiistellige",
|
||||
"settings.email_alerts_for_bans": "Email-Benachrichtigunge für Sperrige aktiviere",
|
||||
"settings.email_alerts_for_unbans": "Email-Benachrichtigunge für Entsperrige aktiviere",
|
||||
"settings.smtp": "SMTP-Konfiguration",
|
||||
@@ -198,6 +199,23 @@
|
||||
"settings.save": "Speicherä",
|
||||
"modal.filter_config": "Filter / Jail-Konfiguration:",
|
||||
"modal.filter_config_edit": "Filter / Jail bearbeite",
|
||||
"modal.filter_config_label": "Filter-Konfiguration",
|
||||
"modal.filter_config_hint": "Wenn leer glah, wird e leeri Filterdatei erstellt.",
|
||||
"modal.filter_name": "Filter-Name",
|
||||
"modal.filter_name_hint": "Nur alphanumerischi Zeiche, Bindestrich und Unterstriche si erlaubt.",
|
||||
"modal.jail_config": "Jail-Konfiguration",
|
||||
"modal.jail_config_hint": "D Jail-Konfiguration wird automatisch usgfüllt, wenn Sie ä Filter uswähle.",
|
||||
"modal.jail_config_label": "Jail-Konfiguration",
|
||||
"modal.jail_filter": "Filter (optional)",
|
||||
"modal.jail_filter_hint": "D Uswahl von ne Filter füllt d Jail-Konfiguration automatisch us.",
|
||||
"modal.jail_name": "Jail-Name",
|
||||
"modal.jail_name_hint": "Nur alphanumerischi Zeiche, Bindestrich und Unterstriche si erlaubt.",
|
||||
"modal.test_logpath": "Logpfad teste",
|
||||
"modal.create": "Ersteue",
|
||||
"modal.create_filter": "Neue Filter ersteue",
|
||||
"modal.create_filter_title": "Neue Filter ersteue",
|
||||
"modal.create_jail": "Neus Jail ersteue",
|
||||
"modal.create_jail_title": "Neus Jail ersteue",
|
||||
"modal.cancel": "Abbräche",
|
||||
"modal.save": "Speicherä",
|
||||
"modal.close": "Wider Schliesse",
|
||||
@@ -229,9 +247,10 @@
|
||||
"servers.form.hostname": "Server-Hostname",
|
||||
"servers.form.hostname_placeholder": "optional",
|
||||
"servers.form.ssh_user": "SSH-Benutzer",
|
||||
"servers.form.ssh_user_placeholder": "root",
|
||||
"servers.form.ssh_user_placeholder": "sa_fail2ban",
|
||||
"servers.form.ssh_key": "Pfad zum SSH-Schlüssel",
|
||||
"servers.form.ssh_key_placeholder": "~/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_placeholder": "/config/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_help": "Platzieren Sie Ihren SSH-Private-Key im Verzeichnis /config/.ssh/ (gemountetes Config-Volume). Die Schlüsseldatei muss die Berechtigungen 600 haben (chmod 600). Beispiel: /config/.ssh/id_rsa",
|
||||
"servers.form.agent_url": "Agent-URL",
|
||||
"servers.form.agent_url_placeholder": "https://host:9443",
|
||||
"servers.form.agent_secret": "Agent-Secret",
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Alert Countries",
|
||||
"settings.alert_countries_description": "Choose the countries for which you want to receive email alerts when a block is triggered.",
|
||||
"settings.email_alerts": "Email Alert Preferences",
|
||||
"settings.email_alerts_for_bans": "Enable email alerts for bans",
|
||||
"settings.email_alerts_for_unbans": "Enable email alerts for unbans",
|
||||
"settings.smtp": "SMTP Configuration",
|
||||
@@ -198,6 +199,23 @@
|
||||
"settings.save": "Save",
|
||||
"modal.filter_config": "Filter / Jail Configuration:",
|
||||
"modal.filter_config_edit": "Edit Filter / Jail",
|
||||
"modal.filter_config_label": "Filter Configuration",
|
||||
"modal.filter_config_hint": "If left empty, an empty filter file will be created.",
|
||||
"modal.filter_name": "Filter Name",
|
||||
"modal.filter_name_hint": "Only alphanumeric characters, dashes, and underscores are allowed.",
|
||||
"modal.jail_config": "Jail Configuration",
|
||||
"modal.jail_config_hint": "Jail configuration will be auto-populated when you select a filter.",
|
||||
"modal.jail_config_label": "Jail Configuration",
|
||||
"modal.jail_filter": "Filter (optional)",
|
||||
"modal.jail_filter_hint": "Selecting a filter will auto-populate the jail configuration.",
|
||||
"modal.jail_name": "Jail Name",
|
||||
"modal.jail_name_hint": "Only alphanumeric characters, dashes, and underscores are allowed.",
|
||||
"modal.test_logpath": "Test Logpath",
|
||||
"modal.create": "Create",
|
||||
"modal.create_filter": "Create New Filter",
|
||||
"modal.create_filter_title": "Create New Filter",
|
||||
"modal.create_jail": "Create New Jail",
|
||||
"modal.create_jail_title": "Create New Jail",
|
||||
"modal.cancel": "Cancel",
|
||||
"modal.save": "Save",
|
||||
"modal.close": "Close",
|
||||
@@ -229,9 +247,10 @@
|
||||
"servers.form.hostname": "Server Hostname",
|
||||
"servers.form.hostname_placeholder": "optional",
|
||||
"servers.form.ssh_user": "SSH User",
|
||||
"servers.form.ssh_user_placeholder": "root",
|
||||
"servers.form.ssh_user_placeholder": "sa_fail2ban",
|
||||
"servers.form.ssh_key": "SSH Private Key Path",
|
||||
"servers.form.ssh_key_placeholder": "~/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_placeholder": "/config/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_help": "Place your SSH private key in the /config/.ssh/ directory (mounted config volume). The key file must have permissions 600 (chmod 600). Example: /config/.ssh/id_rsa",
|
||||
"servers.form.agent_url": "Agent URL",
|
||||
"servers.form.agent_url_placeholder": "https://host:9443",
|
||||
"servers.form.agent_secret": "Agent Secret",
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Países para alerta",
|
||||
"settings.alert_countries_description": "Elige los países para los que deseas recibir alertas por correo electrónico cuando se produzca un bloqueo.",
|
||||
"settings.email_alerts": "Preferencias de alertas por email",
|
||||
"settings.email_alerts_for_bans": "Activar alertas por email para bloqueos",
|
||||
"settings.email_alerts_for_unbans": "Activar alertas por email para desbloqueos",
|
||||
"settings.smtp": "Configuración SMTP",
|
||||
@@ -198,6 +199,23 @@
|
||||
"settings.save": "Guardar",
|
||||
"modal.filter_config": "Configuración del filtro / Jail:",
|
||||
"modal.filter_config_edit": "Editar filtro / Jail",
|
||||
"modal.filter_config_label": "Configuración del filtro",
|
||||
"modal.filter_config_hint": "Si se deja vacío, se creará un archivo de filtro vacío.",
|
||||
"modal.filter_name": "Nombre del filtro",
|
||||
"modal.filter_name_hint": "Solo se permiten caracteres alfanuméricos, guiones y guiones bajos.",
|
||||
"modal.jail_config": "Configuración del jail",
|
||||
"modal.jail_config_hint": "La configuración del jail se completará automáticamente cuando seleccione un filtro.",
|
||||
"modal.jail_config_label": "Configuración del jail",
|
||||
"modal.jail_filter": "Filtro (opcional)",
|
||||
"modal.jail_filter_hint": "La selección de un filtro completará automáticamente la configuración del jail.",
|
||||
"modal.jail_name": "Nombre del jail",
|
||||
"modal.jail_name_hint": "Solo se permiten caracteres alfanuméricos, guiones y guiones bajos.",
|
||||
"modal.test_logpath": "Probar ruta de registro",
|
||||
"modal.create": "Crear",
|
||||
"modal.create_filter": "Crear nuevo filtro",
|
||||
"modal.create_filter_title": "Crear nuevo filtro",
|
||||
"modal.create_jail": "Crear nuevo jail",
|
||||
"modal.create_jail_title": "Crear nuevo jail",
|
||||
"modal.cancel": "Cancelar",
|
||||
"modal.save": "Guardar",
|
||||
"modal.close": "Cerrar",
|
||||
@@ -229,9 +247,10 @@
|
||||
"servers.form.hostname": "Nombre de host del servidor",
|
||||
"servers.form.hostname_placeholder": "opcional",
|
||||
"servers.form.ssh_user": "Usuario SSH",
|
||||
"servers.form.ssh_user_placeholder": "root",
|
||||
"servers.form.ssh_user_placeholder": "sa_fail2ban",
|
||||
"servers.form.ssh_key": "Ruta de la clave SSH",
|
||||
"servers.form.ssh_key_placeholder": "~/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_placeholder": "/config/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_help": "Coloque su clave privada SSH en el directorio /config/.ssh/ (volumen de configuración montado). El archivo de clave debe tener permisos 600 (chmod 600). Ejemplo: /config/.ssh/id_rsa",
|
||||
"servers.form.agent_url": "URL del agente",
|
||||
"servers.form.agent_url_placeholder": "https://host:9443",
|
||||
"servers.form.agent_secret": "Secreto del agente",
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Pays d'alerte",
|
||||
"settings.alert_countries_description": "Choisissez les pays pour lesquels vous souhaitez recevoir des alertes par email lors d'un blocage.",
|
||||
"settings.email_alerts": "Préférences d'alertes email",
|
||||
"settings.email_alerts_for_bans": "Activer les alertes email pour les bannissements",
|
||||
"settings.email_alerts_for_unbans": "Activer les alertes email pour les débannissements",
|
||||
"settings.smtp": "Configuration SMTP",
|
||||
@@ -198,6 +199,23 @@
|
||||
"settings.save": "Enregistrer",
|
||||
"modal.filter_config": "Configuration du filtre / Jail:",
|
||||
"modal.filter_config_edit": "Modifier le filtre / Jail",
|
||||
"modal.filter_config_label": "Configuration du filtre",
|
||||
"modal.filter_config_hint": "Si laissé vide, un fichier de filtre vide sera créé.",
|
||||
"modal.filter_name": "Nom du filtre",
|
||||
"modal.filter_name_hint": "Seuls les caractères alphanumériques, les tirets et les underscores sont autorisés.",
|
||||
"modal.jail_config": "Configuration du jail",
|
||||
"modal.jail_config_hint": "La configuration du jail sera automatiquement remplie lorsque vous sélectionnez un filtre.",
|
||||
"modal.jail_config_label": "Configuration du jail",
|
||||
"modal.jail_filter": "Filtre (optionnel)",
|
||||
"modal.jail_filter_hint": "La sélection d'un filtre remplira automatiquement la configuration du jail.",
|
||||
"modal.jail_name": "Nom du jail",
|
||||
"modal.jail_name_hint": "Seuls les caractères alphanumériques, les tirets et les underscores sont autorisés.",
|
||||
"modal.test_logpath": "Tester le chemin de journal",
|
||||
"modal.create": "Créer",
|
||||
"modal.create_filter": "Créer un nouveau filtre",
|
||||
"modal.create_filter_title": "Créer un nouveau filtre",
|
||||
"modal.create_jail": "Créer un nouveau jail",
|
||||
"modal.create_jail_title": "Créer un nouveau jail",
|
||||
"modal.cancel": "Annuler",
|
||||
"modal.save": "Enregistrer",
|
||||
"modal.close": "Fermer",
|
||||
@@ -229,9 +247,10 @@
|
||||
"servers.form.hostname": "Nom d'hôte du serveur",
|
||||
"servers.form.hostname_placeholder": "optionnel",
|
||||
"servers.form.ssh_user": "Utilisateur SSH",
|
||||
"servers.form.ssh_user_placeholder": "root",
|
||||
"servers.form.ssh_user_placeholder": "sa_fail2ban",
|
||||
"servers.form.ssh_key": "Chemin de la clé SSH",
|
||||
"servers.form.ssh_key_placeholder": "~/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_placeholder": "/config/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_help": "Placez votre clé privée SSH dans le répertoire /config/.ssh/ (volume de configuration monté). Le fichier de clé doit avoir les permissions 600 (chmod 600). Exemple : /config/.ssh/id_rsa",
|
||||
"servers.form.agent_url": "URL de l'agent",
|
||||
"servers.form.agent_url_placeholder": "https://host:9443",
|
||||
"servers.form.agent_secret": "Secret de l'agent",
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
|
||||
"settings.alert_countries": "Paesi per allarme",
|
||||
"settings.alert_countries_description": "Seleziona i paesi per i quali desideri ricevere allarmi via email quando si verifica un blocco.",
|
||||
"settings.email_alerts": "Preferenze allarmi email",
|
||||
"settings.email_alerts_for_bans": "Abilita allarmi email per i ban",
|
||||
"settings.email_alerts_for_unbans": "Abilita allarmi email per gli unban",
|
||||
"settings.smtp": "Configurazione SMTP",
|
||||
@@ -198,6 +199,23 @@
|
||||
"settings.save": "Salva",
|
||||
"modal.filter_config": "Configurazione del filtro / Jail:",
|
||||
"modal.filter_config_edit": "Modifica filtro / Jail",
|
||||
"modal.filter_config_label": "Configurazione del filtro",
|
||||
"modal.filter_config_hint": "Se lasciato vuoto, verrà creato un file di filtro vuoto.",
|
||||
"modal.filter_name": "Nome del filtro",
|
||||
"modal.filter_name_hint": "Sono consentiti solo caratteri alfanumerici, trattini e underscore.",
|
||||
"modal.jail_config": "Configurazione del jail",
|
||||
"modal.jail_config_hint": "La configurazione del jail verrà compilata automaticamente quando si seleziona un filtro.",
|
||||
"modal.jail_config_label": "Configurazione del jail",
|
||||
"modal.jail_filter": "Filtro (opzionale)",
|
||||
"modal.jail_filter_hint": "La selezione di un filtro compila automaticamente la configurazione del jail.",
|
||||
"modal.jail_name": "Nome del jail",
|
||||
"modal.jail_name_hint": "Sono consentiti solo caratteri alfanumerici, trattini e underscore.",
|
||||
"modal.test_logpath": "Testa il percorso del log",
|
||||
"modal.create": "Crea",
|
||||
"modal.create_filter": "Crea nuovo filtro",
|
||||
"modal.create_filter_title": "Crea nuovo filtro",
|
||||
"modal.create_jail": "Crea nuovo jail",
|
||||
"modal.create_jail_title": "Crea nuovo jail",
|
||||
"modal.cancel": "Annulla",
|
||||
"modal.save": "Salva",
|
||||
"modal.close": "Chiudi",
|
||||
@@ -229,9 +247,10 @@
|
||||
"servers.form.hostname": "Nome host del server",
|
||||
"servers.form.hostname_placeholder": "opzionale",
|
||||
"servers.form.ssh_user": "Utente SSH",
|
||||
"servers.form.ssh_user_placeholder": "root",
|
||||
"servers.form.ssh_user_placeholder": "sa_fail2ban",
|
||||
"servers.form.ssh_key": "Percorso della chiave SSH",
|
||||
"servers.form.ssh_key_placeholder": "~/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_placeholder": "/config/.ssh/id_rsa",
|
||||
"servers.form.ssh_key_help": "Posiziona la tua chiave privata SSH nella directory /config/.ssh/ (volume di configurazione montato). Il file della chiave deve avere i permessi 600 (chmod 600). Esempio: /config/.ssh/id_rsa",
|
||||
"servers.form.agent_url": "URL dell'agente",
|
||||
"servers.form.agent_url_placeholder": "https://host:9443",
|
||||
"servers.form.agent_secret": "Segreto dell'agente",
|
||||
|
||||
@@ -153,6 +153,12 @@ func Init(dbPath string) error {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure .ssh directory exists (for SSH key storage)
|
||||
if err := ensureSSHDirectory(); err != nil {
|
||||
// Log but don't fail - .ssh directory creation is not critical
|
||||
log.Printf("Warning: failed to ensure .ssh directory: %v", err)
|
||||
}
|
||||
|
||||
var err error
|
||||
db, err = sql.Open("sqlite", fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout=5000", dbPath))
|
||||
if err != nil {
|
||||
@@ -943,6 +949,31 @@ func ensureDirectory(path string) error {
|
||||
return os.MkdirAll(dir, 0o755)
|
||||
}
|
||||
|
||||
// ensureSSHDirectory ensures the .ssh directory exists for SSH key storage.
|
||||
// In containers, this is /config/.ssh, on the host it's ~/.ssh
|
||||
func ensureSSHDirectory() error {
|
||||
var sshDir string
|
||||
// Check if running inside a container
|
||||
if _, container := os.LookupEnv("CONTAINER"); container {
|
||||
// In container, use /config/.ssh
|
||||
sshDir = "/config/.ssh"
|
||||
} else {
|
||||
// On host, use ~/.ssh
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
sshDir = filepath.Join(home, ".ssh")
|
||||
}
|
||||
|
||||
// Create directory with proper permissions (0700 for .ssh)
|
||||
if err := os.MkdirAll(sshDir, 0o700); err != nil {
|
||||
return fmt.Errorf("failed to create .ssh directory at %s: %w", sshDir, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertPermanentBlock records or updates a permanent block entry.
|
||||
func UpsertPermanentBlock(ctx context.Context, rec PermanentBlockRecord) error {
|
||||
if db == nil {
|
||||
|
||||
@@ -221,7 +221,7 @@ mark {
|
||||
}
|
||||
|
||||
#statusDot.bg-red-500 {
|
||||
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
|
||||
box-shadow: 0 0 0 0 rgb(163 44 44);
|
||||
animation: pulseRed 2s infinite;
|
||||
}
|
||||
|
||||
@@ -420,3 +420,14 @@ button.bg-red-500, button.bg-red-600 {
|
||||
button.bg-red-500:hover, button.bg-red-600:hover {
|
||||
background-color: rgb(220 38 38);
|
||||
}
|
||||
|
||||
/* Green color classes */
|
||||
.bg-green-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.text-green-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
@@ -206,7 +206,7 @@ function resetServerForm() {
|
||||
document.getElementById('serverName').value = '';
|
||||
document.getElementById('serverType').value = 'local';
|
||||
document.getElementById('serverHost').value = '';
|
||||
document.getElementById('serverPort').value = '';
|
||||
document.getElementById('serverPort').value = '22';
|
||||
document.getElementById('serverSocket').value = '/var/run/fail2ban/fail2ban.sock';
|
||||
document.getElementById('serverLogPath').value = '/var/log/fail2ban.log';
|
||||
document.getElementById('serverHostname').value = '';
|
||||
|
||||
@@ -1079,7 +1079,7 @@
|
||||
</div>
|
||||
<div data-server-fields="ssh agent">
|
||||
<label for="serverPort" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.port">Port</label>
|
||||
<input type="number" id="serverPort" min="1" max="65535" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.port_placeholder" placeholder="22">
|
||||
<input type="number" id="serverPort" min="1" max="65535" value="22" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.port_placeholder" placeholder="22">
|
||||
</div>
|
||||
<div data-server-fields="local ssh">
|
||||
<label for="serverSocket" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.socket_path">Fail2ban Socket Path</label>
|
||||
@@ -1095,11 +1095,16 @@
|
||||
</div>
|
||||
<div data-server-fields="ssh">
|
||||
<label for="serverSSHUser" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.ssh_user">SSH User</label>
|
||||
<input type="text" id="serverSSHUser" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.ssh_user_placeholder" placeholder="root">
|
||||
<input type="text" id="serverSSHUser" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.ssh_user_placeholder" placeholder="sa_fail2ban">
|
||||
</div>
|
||||
<div data-server-fields="ssh">
|
||||
<label for="serverSSHKey" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.ssh_key">SSH Private Key Path</label>
|
||||
<input type="text" id="serverSSHKey" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.ssh_key_placeholder" placeholder="~/.ssh/id_rsa">
|
||||
<input type="text" id="serverSSHKey" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.ssh_key_placeholder" placeholder="/config/.ssh/id_rsa">
|
||||
<p class="mt-1 text-sm text-gray-500" data-i18n="servers.form.ssh_key_help">
|
||||
Place your SSH private key in the <code class="px-1 py-0.5 bg-gray-100 rounded text-xs">/config/.ssh/</code> directory (mounted config volume).
|
||||
The key file must have permissions <code class="px-1 py-0.5 bg-gray-100 rounded text-xs">600</code> (chmod 600).
|
||||
Example: <code class="px-1 py-0.5 bg-gray-100 rounded text-xs">/config/.ssh/id_rsa</code>
|
||||
</p>
|
||||
</div>
|
||||
<div data-server-fields="ssh">
|
||||
<label for="serverSSHKeySelect" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.select_key">Select Private Key</label>
|
||||
|
||||
Reference in New Issue
Block a user