remove unsecure sudo executions on remote systems, insted we use facls new

This commit is contained in:
2025-11-14 09:41:43 +01:00
parent 10779e7cea
commit bff920e5b3
3 changed files with 51 additions and 21 deletions

View File

@@ -141,14 +141,34 @@ podman run -d \
- **Host**: IP address or hostname
- **Port**: SSH port (default: 22)
- **SSH User**: Username for SSH connection
- **SSH Key**: Select an SSH key from your `~/.ssh/` directory
- **SSH Key**: Select an SSH key from your `~/.ssh/` directory (or `/config/.ssh/` when running in a container)
5. Click **Test Connection** to verify before saving
6. The UI will automatically deploy the custom action for ban notifications
**Requirements for SSH connections:**
- SSH key-based authentication (passwordless login)
- Passwordless sudo access to `fail2ban-client` and Fail2ban configuration files
- Network connectivity from UI host to remote server
- The SSH user must have:
- Sudo access to run `systemctl restart fail2ban` and `/usr/bin/fail2ban-client` (configured via sudoers)
- File system ACLs on `/etc/fail2ban` for read/write access to configuration files (no sudo needed for file operations)
**Recommended SSH setup (using service account with ACLs):**
```bash
# Create service account
sudo useradd -r -s /bin/bash sa_fail2ban
# Configure sudoers for fail2ban-client and systemctl restart
sudo visudo -f /etc/sudoers.d/fail2ban-ui
# Add:
sa_fail2ban ALL=(ALL) NOPASSWD: /usr/bin/fail2ban-client *
sa_fail2ban ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart fail2ban
# Set ACLs for /etc/fail2ban directory
sudo setfacl -Rm u:sa_fail2ban:rwX /etc/fail2ban
sudo setfacl -dRm u:sa_fail2ban:rwX /etc/fail2ban
```
This setup provides fine-grained permissions without requiring full passwordless sudo access.
#### **API Agent Server** (Future)
- API agent support is planned for future releases
@@ -158,7 +178,10 @@ podman run -d \
## **🔒 Security Considerations**
- Fail2Ban-UI requires **root privileges** or **passwordless sudo** to interact with Fail2Ban sockets.
- Fail2Ban-UI requires **root privileges** or **passwordless sudo** to interact with Fail2Ban sockets (for local connections).
- For SSH connections, use a **dedicated service account** with:
- **Limited sudo access** (only for `fail2ban-client` and `systemctl restart fail2ban`)
- **File system ACLs** on `/etc/fail2ban` for configuration file access (more secure than full sudo)
- **Restrict access** using **firewall rules** or a **reverse proxy** with authentication.
- Ensure that Fail2Ban logs/configs **aren't exposed publicly**.
- For SSH connections, use **SSH key-based authentication** and restrict SSH access.
@@ -200,9 +223,14 @@ semodule -i fail2ban-container-client.pp
```bash
ssh -i ~/.ssh/your_key user@remote-host
```
- Ensure passwordless sudo is configured:
- Ensure the SSH user has proper permissions:
- Check sudo access for `fail2ban-client` and `systemctl restart fail2ban`:
```bash
sudo -l
sudo -l -U sa_fail2ban
```
- Verify ACLs on `/etc/fail2ban`:
```bash
getfacl /etc/fail2ban
```
- Check debug mode in settings for detailed error messages
- Verify the SSH user has access to `/var/run/fail2ban/fail2ban.sock`

View File

@@ -107,9 +107,13 @@ podman exec -it fail2ban-ui ps aux
### SSH Connection Issues
- Verify SSH key authentication works from the host
- Ensure passwordless sudo is configured on the remote server
- Ensure the SSH user has proper permissions on the remote server:
- Sudo access for `fail2ban-client` and `systemctl restart fail2ban` (configured via sudoers)
- File system ACLs on `/etc/fail2ban` for configuration file access
- See the main README for recommended setup with service account and ACLs
- Check debug mode in settings for detailed error messages
- The container needs network access to remote SSH servers
- SSH keys should be placed in `/config/.ssh` directory inside the container
## Contact & Support
For issues, contributions, or feature requests, visit our GitHub repository:

View File

@@ -178,7 +178,7 @@ func (sc *SSHConnector) Restart(ctx context.Context) error {
func (sc *SSHConnector) GetFilterConfig(ctx context.Context, jail string) (string, error) {
path := fmt.Sprintf("/etc/fail2ban/filter.d/%s.conf", jail)
out, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", path})
out, err := sc.runRemoteCommand(ctx, []string{"cat", path})
if err != nil {
return "", fmt.Errorf("failed to read remote filter config: %w", err)
}
@@ -187,7 +187,7 @@ func (sc *SSHConnector) GetFilterConfig(ctx context.Context, jail string) (strin
func (sc *SSHConnector) SetFilterConfig(ctx context.Context, jail, content string) error {
path := fmt.Sprintf("/etc/fail2ban/filter.d/%s.conf", jail)
cmd := fmt.Sprintf("cat <<'EOF' | sudo tee %s >/dev/null\n%s\nEOF", path, content)
cmd := fmt.Sprintf("cat <<'EOF' | tee %s >/dev/null\n%s\nEOF", path, content)
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
return err
}
@@ -204,7 +204,7 @@ func (sc *SSHConnector) ensureAction(ctx context.Context) error {
script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload)
// Base64 encode the entire script to avoid shell escaping issues
scriptB64 := base64.StdEncoding.EncodeToString([]byte(script))
cmd := fmt.Sprintf("echo %s | base64 -d | sudo bash", scriptB64)
cmd := fmt.Sprintf("echo %s | base64 -d | bash", scriptB64)
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
return err
}
@@ -297,14 +297,14 @@ func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
var allJails []JailInfo
// Parse jail.local
jailLocalContent, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", "/etc/fail2ban/jail.local"})
jailLocalContent, err := sc.runRemoteCommand(ctx, []string{"cat", "/etc/fail2ban/jail.local"})
if err == nil {
jails := parseJailConfigContent(jailLocalContent)
allJails = append(allJails, jails...)
}
// Parse jail.d directory
jailDCmd := "sudo find /etc/fail2ban/jail.d -maxdepth 1 -name '*.conf' -type f"
jailDCmd := "find /etc/fail2ban/jail.d -maxdepth 1 -name '*.conf' -type f"
jailDList, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", jailDCmd})
if err == nil && jailDList != "" {
for _, file := range strings.Split(jailDList, "\n") {
@@ -312,7 +312,7 @@ func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
if file == "" {
continue
}
content, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", file})
content, err := sc.runRemoteCommand(ctx, []string{"cat", file})
if err == nil {
jails := parseJailConfigContent(content)
allJails = append(allJails, jails...)
@@ -326,7 +326,7 @@ func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
// UpdateJailEnabledStates implements Connector.
func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map[string]bool) error {
// Read current jail.local
content, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", "/etc/fail2ban/jail.local"})
content, err := sc.runRemoteCommand(ctx, []string{"cat", "/etc/fail2ban/jail.local"})
if err != nil {
return fmt.Errorf("failed to read jail.local: %w", err)
}
@@ -354,16 +354,15 @@ func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map
// Write back
newContent := strings.Join(outputLines, "\n")
cmd := fmt.Sprintf("cat <<'EOF' | sudo tee /etc/fail2ban/jail.local >/dev/null\n%s\nEOF", newContent)
cmd := fmt.Sprintf("cat <<'EOF' | tee /etc/fail2ban/jail.local >/dev/null\n%s\nEOF", newContent)
_, err = sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
return err
}
// GetFilters implements Connector.
func (sc *SSHConnector) GetFilters(ctx context.Context) ([]string, error) {
// Use find with sudo - execute sudo separately to avoid shell issues
// First try with sudo, if that fails, the error will be clear
list, err := sc.runRemoteCommand(ctx, []string{"sudo", "find", "/etc/fail2ban/filter.d", "-maxdepth", "1", "-type", "f"})
// Use find to list filter files
list, err := sc.runRemoteCommand(ctx, []string{"find", "/etc/fail2ban/filter.d", "-maxdepth", "1", "-type", "f"})
if err != nil {
return nil, fmt.Errorf("failed to list filters: %w", err)
}
@@ -431,11 +430,10 @@ func (sc *SSHConnector) TestFilter(ctx context.Context, filterName string, logLi
continue
}
// Use fail2ban-regex: log line as string, filter file path
// Use sudo -s to run a shell that executes the piped command
escapedLine := strconv.Quote(logLine)
escapedPath := strconv.Quote(filterPath)
cmd := fmt.Sprintf("echo %s | fail2ban-regex - %s", escapedLine, escapedPath)
out, err := sc.runRemoteCommand(ctx, []string{"sudo", "sh", "-c", cmd})
out, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", cmd})
// fail2ban-regex returns success (exit 0) if the line matches
// Look for "Lines: 1 lines, 0 ignored, 1 matched" or similar success indicators
if err == nil {