mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
remove unsecure sudo executions on remote systems, insted we use facls new
This commit is contained in:
42
README.md
42
README.md
@@ -141,14 +141,34 @@ podman run -d \
|
|||||||
- **Host**: IP address or hostname
|
- **Host**: IP address or hostname
|
||||||
- **Port**: SSH port (default: 22)
|
- **Port**: SSH port (default: 22)
|
||||||
- **SSH User**: Username for SSH connection
|
- **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
|
5. Click **Test Connection** to verify before saving
|
||||||
6. The UI will automatically deploy the custom action for ban notifications
|
6. The UI will automatically deploy the custom action for ban notifications
|
||||||
|
|
||||||
**Requirements for SSH connections:**
|
**Requirements for SSH connections:**
|
||||||
- SSH key-based authentication (passwordless login)
|
- SSH key-based authentication (passwordless login)
|
||||||
- Passwordless sudo access to `fail2ban-client` and Fail2ban configuration files
|
|
||||||
- Network connectivity from UI host to remote server
|
- 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 Server** (Future)
|
||||||
- API agent support is planned for future releases
|
- API agent support is planned for future releases
|
||||||
@@ -158,7 +178,10 @@ podman run -d \
|
|||||||
|
|
||||||
## **🔒 Security Considerations**
|
## **🔒 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.
|
- **Restrict access** using **firewall rules** or a **reverse proxy** with authentication.
|
||||||
- Ensure that Fail2Ban logs/configs **aren't exposed publicly**.
|
- Ensure that Fail2Ban logs/configs **aren't exposed publicly**.
|
||||||
- For SSH connections, use **SSH key-based authentication** and restrict SSH access.
|
- For SSH connections, use **SSH key-based authentication** and restrict SSH access.
|
||||||
@@ -200,10 +223,15 @@ semodule -i fail2ban-container-client.pp
|
|||||||
```bash
|
```bash
|
||||||
ssh -i ~/.ssh/your_key user@remote-host
|
ssh -i ~/.ssh/your_key user@remote-host
|
||||||
```
|
```
|
||||||
- Ensure passwordless sudo is configured:
|
- Ensure the SSH user has proper permissions:
|
||||||
```bash
|
- Check sudo access for `fail2ban-client` and `systemctl restart fail2ban`:
|
||||||
sudo -l
|
```bash
|
||||||
```
|
sudo -l -U sa_fail2ban
|
||||||
|
```
|
||||||
|
- Verify ACLs on `/etc/fail2ban`:
|
||||||
|
```bash
|
||||||
|
getfacl /etc/fail2ban
|
||||||
|
```
|
||||||
- Check debug mode in settings for detailed error messages
|
- Check debug mode in settings for detailed error messages
|
||||||
- Verify the SSH user has access to `/var/run/fail2ban/fail2ban.sock`
|
- Verify the SSH user has access to `/var/run/fail2ban/fail2ban.sock`
|
||||||
|
|
||||||
|
|||||||
@@ -107,9 +107,13 @@ podman exec -it fail2ban-ui ps aux
|
|||||||
|
|
||||||
### SSH Connection Issues
|
### SSH Connection Issues
|
||||||
- Verify SSH key authentication works from the host
|
- 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
|
- Check debug mode in settings for detailed error messages
|
||||||
- The container needs network access to remote SSH servers
|
- The container needs network access to remote SSH servers
|
||||||
|
- SSH keys should be placed in `/config/.ssh` directory inside the container
|
||||||
|
|
||||||
## Contact & Support
|
## Contact & Support
|
||||||
For issues, contributions, or feature requests, visit our GitHub repository:
|
For issues, contributions, or feature requests, visit our GitHub repository:
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ func (sc *SSHConnector) Restart(ctx context.Context) error {
|
|||||||
|
|
||||||
func (sc *SSHConnector) GetFilterConfig(ctx context.Context, jail string) (string, error) {
|
func (sc *SSHConnector) GetFilterConfig(ctx context.Context, jail string) (string, error) {
|
||||||
path := fmt.Sprintf("/etc/fail2ban/filter.d/%s.conf", jail)
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read remote filter config: %w", err)
|
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 {
|
func (sc *SSHConnector) SetFilterConfig(ctx context.Context, jail, content string) error {
|
||||||
path := fmt.Sprintf("/etc/fail2ban/filter.d/%s.conf", jail)
|
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})
|
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -204,7 +204,7 @@ func (sc *SSHConnector) ensureAction(ctx context.Context) error {
|
|||||||
script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload)
|
script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload)
|
||||||
// Base64 encode the entire script to avoid shell escaping issues
|
// Base64 encode the entire script to avoid shell escaping issues
|
||||||
scriptB64 := base64.StdEncoding.EncodeToString([]byte(script))
|
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})
|
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -297,14 +297,14 @@ func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
|
|||||||
var allJails []JailInfo
|
var allJails []JailInfo
|
||||||
|
|
||||||
// Parse jail.local
|
// 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 {
|
if err == nil {
|
||||||
jails := parseJailConfigContent(jailLocalContent)
|
jails := parseJailConfigContent(jailLocalContent)
|
||||||
allJails = append(allJails, jails...)
|
allJails = append(allJails, jails...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse jail.d directory
|
// 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})
|
jailDList, err := sc.runRemoteCommand(ctx, []string{"sh", "-c", jailDCmd})
|
||||||
if err == nil && jailDList != "" {
|
if err == nil && jailDList != "" {
|
||||||
for _, file := range strings.Split(jailDList, "\n") {
|
for _, file := range strings.Split(jailDList, "\n") {
|
||||||
@@ -312,7 +312,7 @@ func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
|
|||||||
if file == "" {
|
if file == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
content, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", file})
|
content, err := sc.runRemoteCommand(ctx, []string{"cat", file})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
jails := parseJailConfigContent(content)
|
jails := parseJailConfigContent(content)
|
||||||
allJails = append(allJails, jails...)
|
allJails = append(allJails, jails...)
|
||||||
@@ -326,7 +326,7 @@ func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
|
|||||||
// UpdateJailEnabledStates implements Connector.
|
// UpdateJailEnabledStates implements Connector.
|
||||||
func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map[string]bool) error {
|
func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map[string]bool) error {
|
||||||
// Read current jail.local
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read jail.local: %w", err)
|
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
|
// Write back
|
||||||
newContent := strings.Join(outputLines, "\n")
|
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})
|
_, err = sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFilters implements Connector.
|
// GetFilters implements Connector.
|
||||||
func (sc *SSHConnector) GetFilters(ctx context.Context) ([]string, error) {
|
func (sc *SSHConnector) GetFilters(ctx context.Context) ([]string, error) {
|
||||||
// Use find with sudo - execute sudo separately to avoid shell issues
|
// Use find to list filter files
|
||||||
// First try with sudo, if that fails, the error will be clear
|
list, err := sc.runRemoteCommand(ctx, []string{"find", "/etc/fail2ban/filter.d", "-maxdepth", "1", "-type", "f"})
|
||||||
list, err := sc.runRemoteCommand(ctx, []string{"sudo", "find", "/etc/fail2ban/filter.d", "-maxdepth", "1", "-type", "f"})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list filters: %w", err)
|
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
|
continue
|
||||||
}
|
}
|
||||||
// Use fail2ban-regex: log line as string, filter file path
|
// 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)
|
escapedLine := strconv.Quote(logLine)
|
||||||
escapedPath := strconv.Quote(filterPath)
|
escapedPath := strconv.Quote(filterPath)
|
||||||
cmd := fmt.Sprintf("echo %s | fail2ban-regex - %s", escapedLine, escapedPath)
|
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
|
// fail2ban-regex returns success (exit 0) if the line matches
|
||||||
// Look for "Lines: 1 lines, 0 ignored, 1 matched" or similar success indicators
|
// Look for "Lines: 1 lines, 0 ignored, 1 matched" or similar success indicators
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user