Fail2Ban is a popular intrusion prevention framework that protects servers from brute-force attacks. This Java implementation provides similar functionality for Java applications, monitoring authentication attempts and automatically blocking malicious IP addresses.
Understanding Fail2Ban Concepts
Key Components:
- Log Monitoring: Track authentication attempts and failures
- IP Address Tracking: Monitor suspicious activity per IP
- Ban Rules: Define thresholds and time windows
- Auto-blocking: Temporarily or permanently block malicious IPs
- Jail Management: Different rules for different services
Java Dependencies Setup
<dependencies> <!-- Spring Boot for web application --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security for authentication --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Redis for distributed IP blocking --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>5.1.0</version> </dependency> <!-- JSON processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.16.1</version> </dependency> <!-- Logging --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <!-- Scheduling for cleanup tasks --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> </dependencies>
Core Fail2Ban Models and Configuration
package com.example.fail2ban;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.*;
import java.util.concurrent.TimeUnit;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Fail2BanConfig {
private String name;
private String description;
private int maxRetries;
private long findTime; // Time window in milliseconds
private long banTime; // Ban duration in milliseconds
private boolean enabled;
private List<String> whitelist;
private List<String> blacklist;
private BanAction action;
private NotificationSettings notification;
public enum BanAction {
BLOCK_TEMPORARY, BLOCK_PERMANENT, REDIRECT, CAPTCHA, NOTIFY
}
// Constructors
public Fail2BanConfig() {
this.maxRetries = 5;
this.findTime = TimeUnit.MINUTES.toMillis(10);
this.banTime = TimeUnit.MINUTES.toMillis(30);
this.enabled = true;
this.whitelist = new ArrayList<>();
this.blacklist = new ArrayList<>();
this.action = BanAction.BLOCK_TEMPORARY;
this.notification = new NotificationSettings();
}
public Fail2BanConfig(String name, int maxRetries, long findTimeMinutes, long banTimeMinutes) {
this();
this.name = name;
this.maxRetries = maxRetries;
this.findTime = TimeUnit.MINUTES.toMillis(findTimeMinutes);
this.banTime = TimeUnit.MINUTES.toMillis(banTimeMinutes);
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public int getMaxRetries() { return maxRetries; }
public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; }
public long getFindTime() { return findTime; }
public void setFindTime(long findTime) { this.findTime = findTime; }
public long getBanTime() { return banTime; }
public void setBanTime(long banTime) { this.banTime = banTime; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public List<String> getWhitelist() { return whitelist; }
public void setWhitelist(List<String> whitelist) { this.whitelist = whitelist; }
public List<String> getBlacklist() { return blacklist; }
public void setBlacklist(List<String> blacklist) { this.blacklist = blacklist; }
public BanAction getAction() { return action; }
public void setAction(BanAction action) { this.action = action; }
public NotificationSettings getNotification() { return notification; }
public void setNotification(NotificationSettings notification) { this.notification = notification; }
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class NotificationSettings {
private boolean enabled;
private String email;
private String webhookUrl;
private List<String> notifyOn;
public NotificationSettings() {
this.enabled = false;
this.notifyOn = Arrays.asList("BAN", "UNBAN");
}
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getWebhookUrl() { return webhookUrl; }
public void setWebhookUrl(String webhookUrl) { this.webhookUrl = webhookUrl; }
public List<String> getNotifyOn() { return notifyOn; }
public void setNotifyOn(List<String> notifyOn) { this.notifyOn = notifyOn; }
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AuthenticationAttempt {
private String ipAddress;
private String username;
private boolean success;
private Date timestamp;
private String userAgent;
private String endpoint;
private Map<String, Object> context;
// Constructors
public AuthenticationAttempt() {
this.timestamp = new Date();
this.context = new HashMap<>();
}
public AuthenticationAttempt(String ipAddress, String username, boolean success) {
this();
this.ipAddress = ipAddress;
this.username = username;
this.success = success;
}
// Getters and setters
public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public Date getTimestamp() { return timestamp; }
public void setTimestamp(Date timestamp) { this.timestamp = timestamp; }
public String getUserAgent() { return userAgent; }
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
public String getEndpoint() { return endpoint; }
public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
public Map<String, Object> getContext() { return context; }
public void setContext(Map<String, Object> context) { this.context = context; }
// Builder methods
public AuthenticationAttempt withUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public AuthenticationAttempt withEndpoint(String endpoint) {
this.endpoint = endpoint;
return this;
}
public AuthenticationAttempt withContext(String key, Object value) {
this.context.put(key, value);
return this;
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BanRecord {
private String ipAddress;
private Date banTime;
private Date unbanTime;
private String reason;
private int failureCount;
private String jailName;
private boolean active;
private List<String> attemptedUsernames;
// Constructors
public BanRecord() {
this.banTime = new Date();
this.attemptedUsernames = new ArrayList<>();
this.active = true;
}
public BanRecord(String ipAddress, String reason, int failureCount, String jailName) {
this();
this.ipAddress = ipAddress;
this.reason = reason;
this.failureCount = failureCount;
this.jailName = jailName;
}
// Getters and setters
public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
public Date getBanTime() { return banTime; }
public void setBanTime(Date banTime) { this.banTime = banTime; }
public Date getUnbanTime() { return unbanTime; }
public void setUnbanTime(Date unbanTime) { this.unbanTime = unbanTime; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
public int getFailureCount() { return failureCount; }
public void setFailureCount(int failureCount) { this.failureCount = failureCount; }
public String getJailName() { return jailName; }
public void setJailName(String jailName) { this.jailName = jailName; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
public List<String> getAttemptedUsernames() { return attemptedUsernames; }
public void setAttemptedUsernames(List<String> attemptedUsernames) { this.attemptedUsernames = attemptedUsernames; }
// Helper methods
public void addAttemptedUsername(String username) {
if (!this.attemptedUsernames.contains(username)) {
this.attemptedUsernames.add(username);
}
}
public boolean isExpired() {
return unbanTime != null && new Date().after(unbanTime);
}
}
Core Fail2Ban Service
package com.example.fail2ban;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Fail2BanService {
private static final Logger logger = LoggerFactory.getLogger(Fail2BanService.class);
private final Map<String, Fail2BanConfig> jails;
private final JedisPool jedisPool;
private final ScheduledExecutorService scheduler;
// In-memory storage for IP tracking (supplemented by Redis for persistence)
private final Map<String, List<AuthenticationAttempt>> ipAttempts;
private final Map<String, BanRecord> activeBans;
// Statistics
private final Map<String, JailStatistics> jailStatistics;
public Fail2BanService(String redisHost, int redisPort) {
this.jails = new ConcurrentHashMap<>();
this.ipAttempts = new ConcurrentHashMap<>();
this.activeBans = new ConcurrentHashMap<>();
this.jailStatistics = new ConcurrentHashMap<>();
// Initialize Redis connection pool
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(20);
poolConfig.setMaxIdle(10);
this.jedisPool = new JedisPool(poolConfig, redisHost, redisPort);
this.scheduler = Executors.newScheduledThreadPool(2);
initializeDefaultJails();
startCleanupTasks();
}
/**
* Record an authentication attempt
*/
public void recordAttempt(AuthenticationAttempt attempt) {
String ipAddress = attempt.getIpAddress();
// Skip if IP is whitelisted
if (isWhitelisted(ipAddress)) {
logger.debug("IP {} is whitelisted, skipping fail2ban tracking", ipAddress);
return;
}
// Check if IP is already banned
if (isBanned(ipAddress)) {
logger.warn("Blocked authentication attempt from banned IP: {}", ipAddress);
updateStatistics(ipAddress, "BLOCKED_ATTEMPT");
return;
}
// Store attempt
ipAttempts.computeIfAbsent(ipAddress, k -> new ArrayList<>()).add(attempt);
// Check all jails for this IP
for (Fail2BanConfig jail : jails.values()) {
if (jail.isEnabled()) {
checkJail(jail, ipAddress);
}
}
// Also store in Redis for persistence
storeAttemptInRedis(attempt);
}
/**
* Check if an IP address is currently banned
*/
public boolean isBanned(String ipAddress) {
// Check in-memory cache first
BanRecord ban = activeBans.get(ipAddress);
if (ban != null) {
if (ban.isExpired()) {
activeBans.remove(ipAddress);
return false;
}
return ban.isActive();
}
// Check Redis
return checkBanInRedis(ipAddress);
}
/**
* Manually ban an IP address
*/
public void banIp(String ipAddress, String reason, String jailName, long durationMinutes) {
BanRecord banRecord = new BanRecord(ipAddress, reason, 0, jailName);
banRecord.setUnbanTime(new Date(System.currentTimeMillis() +
TimeUnit.MINUTES.toMillis(durationMinutes)));
activeBans.put(ipAddress, banRecord);
storeBanInRedis(banRecord);
logger.info("Manually banned IP {} for {} minutes. Reason: {}",
ipAddress, durationMinutes, reason);
// Send notification
sendNotification("IP_BANNED", ipAddress, jailName, reason);
updateStatistics(ipAddress, "MANUAL_BAN");
}
/**
* Unban an IP address
*/
public void unbanIp(String ipAddress) {
BanRecord ban = activeBans.remove(ipAddress);
if (ban != null) {
ban.setActive(false);
ban.setUnbanTime(new Date());
removeBanFromRedis(ipAddress);
logger.info("Unbanned IP: {}", ipAddress);
sendNotification("IP_UNBANNED", ipAddress, ban.getJailName(), "Manual unban");
}
}
/**
* Get ban statistics for an IP
*/
public IpStatistics getIpStatistics(String ipAddress) {
IpStatistics stats = new IpStatistics();
stats.setIpAddress(ipAddress);
List<AuthenticationAttempt> attempts = ipAttempts.get(ipAddress);
if (attempts != null) {
long totalAttempts = attempts.size();
long failedAttempts = attempts.stream().filter(a -> !a.isSuccess()).count();
long successfulAttempts = attempts.stream().filter(AuthenticationAttempt::isSuccess).count();
stats.setTotalAttempts(totalAttempts);
stats.setFailedAttempts(failedAttempts);
stats.setSuccessfulAttempts(successfulAttempts);
stats.setFailureRate(totalAttempts > 0 ? (double) failedAttempts / totalAttempts : 0.0);
// Find first and last attempt
attempts.stream()
.min(Comparator.comparing(AuthenticationAttempt::getTimestamp))
.ifPresent(first -> stats.setFirstAttempt(first.getTimestamp()));
attempts.stream()
.max(Comparator.comparing(AuthenticationAttempt::getTimestamp))
.ifPresent(last -> stats.setLastAttempt(last.getTimestamp()));
}
BanRecord currentBan = activeBans.get(ipAddress);
stats.setCurrentlyBanned(currentBan != null && currentBan.isActive());
if (currentBan != null) {
stats.setBanReason(currentBan.getReason());
stats.setBanTime(currentBan.getBanTime());
stats.setUnbanTime(currentBan.getUnbanTime());
}
return stats;
}
/**
* Add a new jail configuration
*/
public void addJail(Fail2BanConfig jail) {
jails.put(jail.getName(), jail);
jailStatistics.put(jail.getName(), new JailStatistics(jail.getName()));
logger.info("Added jail configuration: {}", jail.getName());
}
/**
* Remove a jail configuration
*/
public void removeJail(String jailName) {
jails.remove(jailName);
jailStatistics.remove(jailName);
logger.info("Removed jail configuration: {}", jailName);
}
private void checkJail(Fail2BanConfig jail, String ipAddress) {
List<AuthenticationAttempt> attempts = ipAttempts.get(ipAddress);
if (attempts == null) return;
// Filter attempts within the findTime window
long windowStart = System.currentTimeMillis() - jail.getFindTime();
List<AuthenticationAttempt> recentAttempts = attempts.stream()
.filter(attempt -> attempt.getTimestamp().getTime() >= windowStart)
.filter(attempt -> !attempt.isSuccess()) // Only count failures
.toList();
int failureCount = recentAttempts.size();
if (failureCount >= jail.getMaxRetries()) {
// Ban the IP
BanRecord banRecord = new BanRecord(
ipAddress,
"Exceeded maximum authentication attempts",
failureCount,
jail.getName()
);
// Collect attempted usernames
recentAttempts.stream()
.map(AuthenticationAttempt::getUsername)
.filter(Objects::nonNull)
.forEach(banRecord::addAttemptedUsername);
// Set unban time
banRecord.setUnbanTime(new Date(System.currentTimeMillis() + jail.getBanTime()));
// Store ban
activeBans.put(ipAddress, banRecord);
storeBanInRedis(banRecord);
logger.warn("Banned IP {} for {} minutes. Failures: {}, Jail: {}",
ipAddress, TimeUnit.MILLISECONDS.toMinutes(jail.getBanTime()),
failureCount, jail.getName());
// Send notification
sendNotification("IP_BANNED", ipAddress, jail.getName(),
"Exceeded " + failureCount + " failed attempts");
// Update statistics
updateStatistics(ipAddress, "AUTO_BAN");
jailStatistics.get(jail.getName()).incrementBans();
}
}
private boolean isWhitelisted(String ipAddress) {
for (Fail2BanConfig jail : jails.values()) {
if (jail.getWhitelist().contains(ipAddress)) {
return true;
}
}
return false;
}
private void initializeDefaultJails() {
// Default SSH-style jail
Fail2BanConfig sshJail = new Fail2BanConfig("ssh", 5, 10, 30);
sshJail.setDescription("Protect against SSH brute force attacks");
sshJail.getWhitelist().addAll(Arrays.asList("127.0.0.1", "192.168.1.0/24"));
addJail(sshJail);
// Web application jail
Fail2BanConfig webJail = new Fail2BanConfig("web-auth", 10, 5, 60);
webJail.setDescription("Protect web application authentication");
webJail.setAction(Fail2BanConfig.BanAction.CAPTCHA);
addJail(webJail);
// API jail with stricter limits
Fail2BanConfig apiJail = new Fail2BanConfig("api", 20, 1, 1440); // 24-hour ban
apiJail.setDescription("Protect API endpoints from abuse");
addJail(apiJail);
}
private void startCleanupTasks() {
// Clean up expired bans every minute
scheduler.scheduleAtFixedRate(this::cleanupExpiredBans, 1, 1, TimeUnit.MINUTES);
// Clean up old attempts every 5 minutes
scheduler.scheduleAtFixedRate(this::cleanupOldAttempts, 5, 5, TimeUnit.MINUTES);
// Persist statistics every 10 minutes
scheduler.scheduleAtFixedRate(this::persistStatistics, 10, 10, TimeUnit.MINUTES);
}
private void cleanupExpiredBans() {
Iterator<Map.Entry<String, BanRecord>> iterator = activeBans.entrySet().iterator();
int cleaned = 0;
while (iterator.hasNext()) {
Map.Entry<String, BanRecord> entry = iterator.next();
BanRecord ban = entry.getValue();
if (ban.isExpired()) {
iterator.remove();
removeBanFromRedis(entry.getKey());
cleaned++;
logger.info("Auto-unbanned expired IP: {}", entry.getKey());
sendNotification("IP_UNBANNED", entry.getKey(), ban.getJailName(), "Auto-expired");
}
}
if (cleaned > 0) {
logger.debug("Cleaned up {} expired bans", cleaned);
}
}
private void cleanupOldAttempts() {
long cleanupThreshold = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(24);
for (List<AuthenticationAttempt> attempts : ipAttempts.values()) {
attempts.removeIf(attempt ->
attempt.getTimestamp().getTime() < cleanupThreshold);
}
// Remove empty IP entries
ipAttempts.entrySet().removeIf(entry -> entry.getValue().isEmpty());
}
private void persistStatistics() {
// Persist statistics to Redis or database
try (Jedis jedis = jedisPool.getResource()) {
for (JailStatistics stats : jailStatistics.values()) {
String key = "fail2ban:stats:" + stats.getJailName();
jedis.hset(key, "totalBans", String.valueOf(stats.getTotalBans()));
jedis.hset(key, "lastUpdated", new Date().toString());
jedis.expire(key, TimeUnit.DAYS.toSeconds(7)); // Keep for 7 days
}
} catch (Exception e) {
logger.error("Failed to persist statistics", e);
}
}
// Redis integration methods
private void storeAttemptInRedis(AuthenticationAttempt attempt) {
try (Jedis jedis = jedisPool.getResource()) {
String key = "fail2ban:attempts:" + attempt.getIpAddress();
String attemptJson = serializeToJson(attempt);
jedis.lpush(key, attemptJson);
jedis.ltrim(key, 0, 99); // Keep only last 100 attempts
jedis.expire(key, TimeUnit.DAYS.toSeconds(1)); // Expire after 1 day
} catch (Exception e) {
logger.error("Failed to store attempt in Redis", e);
}
}
private void storeBanInRedis(BanRecord ban) {
try (Jedis jedis = jedisPool.getResource()) {
String key = "fail2ban:bans:" + ban.getIpAddress();
String banJson = serializeToJson(ban);
long ttl = (ban.getUnbanTime().getTime() - System.currentTimeMillis()) / 1000;
jedis.setex(key, ttl, banJson);
} catch (Exception e) {
logger.error("Failed to store ban in Redis", e);
}
}
private boolean checkBanInRedis(String ipAddress) {
try (Jedis jedis = jedisPool.getResource()) {
String key = "fail2ban:bans:" + ipAddress;
return jedis.exists(key);
} catch (Exception e) {
logger.error("Failed to check ban in Redis", e);
return false;
}
}
private void removeBanFromRedis(String ipAddress) {
try (Jedis jedis = jedisPool.getResource()) {
String key = "fail2ban:bans:" + ipAddress;
jedis.del(key);
} catch (Exception e) {
logger.error("Failed to remove ban from Redis", e);
}
}
private void sendNotification(String type, String ipAddress, String jailName, String reason) {
// Implement notification logic (email, Slack, webhook, etc.)
logger.info("Notification {} - IP: {}, Jail: {}, Reason: {}",
type, ipAddress, jailName, reason);
}
private void updateStatistics(String ipAddress, String eventType) {
// Update statistics for monitoring
}
private String serializeToJson(Object obj) {
// Simple JSON serialization (use Jackson in production)
return obj.toString();
}
// Statistics classes
public static class IpStatistics {
private String ipAddress;
private long totalAttempts;
private long failedAttempts;
private long successfulAttempts;
private double failureRate;
private Date firstAttempt;
private Date lastAttempt;
private boolean currentlyBanned;
private String banReason;
private Date banTime;
private Date unbanTime;
// Getters and setters
public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
public long getTotalAttempts() { return totalAttempts; }
public void setTotalAttempts(long totalAttempts) { this.totalAttempts = totalAttempts; }
public long getFailedAttempts() { return failedAttempts; }
public void setFailedAttempts(long failedAttempts) { this.failedAttempts = failedAttempts; }
public long getSuccessfulAttempts() { return successfulAttempts; }
public void setSuccessfulAttempts(long successfulAttempts) { this.successfulAttempts = successfulAttempts; }
public double getFailureRate() { return failureRate; }
public void setFailureRate(double failureRate) { this.failureRate = failureRate; }
public Date getFirstAttempt() { return firstAttempt; }
public void setFirstAttempt(Date firstAttempt) { this.firstAttempt = firstAttempt; }
public Date getLastAttempt() { return lastAttempt; }
public void setLastAttempt(Date lastAttempt) { this.lastAttempt = lastAttempt; }
public boolean isCurrentlyBanned() { return currentlyBanned; }
public void setCurrentlyBanned(boolean currentlyBanned) { this.currentlyBanned = currentlyBanned; }
public String getBanReason() { return banReason; }
public void setBanReason(String banReason) { this.banReason = banReason; }
public Date getBanTime() { return banTime; }
public void setBanTime(Date banTime) { this.banTime = banTime; }
public Date getUnbanTime() { return unbanTime; }
public void setUnbanTime(Date unbanTime) { this.unbanTime = unbanTime; }
}
public static class JailStatistics {
private String jailName;
private long totalBans;
private long totalAttempts;
private Date lastBan;
private Date lastAttempt;
public JailStatistics(String jailName) {
this.jailName = jailName;
}
// Getters and setters
public String getJailName() { return jailName; }
public void setJailName(String jailName) { this.jailName = jailName; }
public long getTotalBans() { return totalBans; }
public void setTotalBans(long totalBans) { this.totalBans = totalBans; }
public long getTotalAttempts() { return totalAttempts; }
public void setTotalAttempts(long totalAttempts) { this.totalAttempts = totalAttempts; }
public Date getLastBan() { return lastBan; }
public void setLastBan(Date lastBan) { this.lastBan = lastBan; }
public Date getLastAttempt() { return lastAttempt; }
public void setLastAttempt(Date lastAttempt) { this.lastAttempt = lastAttempt; }
public void incrementBans() {
this.totalBans++;
this.lastBan = new Date();
}
public void incrementAttempts() {
this.totalAttempts++;
this.lastAttempt = new Date();
}
}
}
Spring Security Integration
package com.example.fail2ban;
import org.springframework.security.authentication.event.*;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Component
public class AuthenticationEventListener {
private final Fail2BanService fail2BanService;
public AuthenticationEventListener(Fail2BanService fail2BanService) {
this.fail2BanService = fail2BanService;
}
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
HttpServletRequest request = getCurrentRequest();
if (request == null) return;
String ipAddress = getClientIpAddress(request);
String username = event.getAuthentication().getName();
AuthenticationAttempt attempt = new AuthenticationAttempt(ipAddress, username, true)
.withUserAgent(request.getHeader("User-Agent"))
.withEndpoint(request.getRequestURI())
.withContext("auth_type", "spring_security");
fail2BanService.recordAttempt(attempt);
}
@EventListener
public void handleAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
HttpServletRequest request = getCurrentRequest();
if (request == null) return;
String ipAddress = getClientIpAddress(request);
String username = event.getAuthentication().getName();
AuthenticationAttempt attempt = new AuthenticationAttempt(ipAddress, username, false)
.withUserAgent(request.getHeader("User-Agent"))
.withEndpoint(request.getRequestURI())
.withContext("auth_type", "spring_security")
.withContext("failure_reason", event.getException().getMessage());
fail2BanService.recordAttempt(attempt);
}
private HttpServletRequest getCurrentRequest() {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes != null ? attributes.getRequest() : null;
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}
return request.getRemoteAddr();
}
}
Spring Boot REST Controller
package com.example.fail2ban;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@RestController
@RequestMapping("/api/fail2ban")
public class Fail2BanController {
private final Fail2BanService fail2BanService;
public Fail2BanController(Fail2BanService fail2BanService) {
this.fail2BanService = fail2BanService;
}
@GetMapping("/check/{ipAddress}")
public ResponseEntity<Map<String, Object>> checkIpStatus(@PathVariable String ipAddress) {
boolean isBanned = fail2BanService.isBanned(ipAddress);
Fail2BanService.IpStatistics stats = fail2BanService.getIpStatistics(ipAddress);
Map<String, Object> response = new HashMap<>();
response.put("ipAddress", ipAddress);
response.put("banned", isBanned);
response.put("statistics", stats);
return ResponseEntity.ok(response);
}
@PostMapping("/ban")
public ResponseEntity<Map<String, Object>> banIpAddress(
@RequestBody BanRequest request) {
fail2BanService.banIp(
request.getIpAddress(),
request.getReason(),
request.getJailName(),
request.getDurationMinutes()
);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "IP address banned successfully"
));
}
@PostMapping("/unban")
public ResponseEntity<Map<String, Object>> unbanIpAddress(
@RequestBody UnbanRequest request) {
fail2BanService.unbanIp(request.getIpAddress());
return ResponseEntity.ok(Map.of(
"success", true,
"message", "IP address unbanned successfully"
));
}
@GetMapping("/statistics/{ipAddress}")
public ResponseEntity<Fail2BanService.IpStatistics> getIpStatistics(
@PathVariable String ipAddress) {
Fail2BanService.IpStatistics stats = fail2BanService.getIpStatistics(ipAddress);
return ResponseEntity.ok(stats);
}
@GetMapping("/banned")
public ResponseEntity<List<Map<String, Object>>> getBannedIps() {
// Return list of currently banned IPs
List<Map<String, Object>> bannedIps = new ArrayList<>();
// Implementation would iterate through active bans
return ResponseEntity.ok(bannedIps);
}
@PostMapping("/test/auth-attempt")
public ResponseEntity<Map<String, Object>> testAuthAttempt(
HttpServletRequest request,
@RequestParam boolean success) {
String ipAddress = getClientIpAddress(request);
String username = request.getParameter("username") != null ?
request.getParameter("username") : "testuser";
AuthenticationAttempt attempt = new AuthenticationAttempt(ipAddress, username, success)
.withUserAgent(request.getHeader("User-Agent"))
.withEndpoint("/api/fail2ban/test/auth-attempt")
.withContext("test", true);
fail2BanService.recordAttempt(attempt);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Test authentication attempt recorded",
"ip", ipAddress,
"banned", fail2BanService.isBanned(ipAddress)
));
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
// Request DTOs
public static class BanRequest {
private String ipAddress;
private String reason;
private String jailName;
private long durationMinutes;
// Getters and setters
public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
public String getJailName() { return jailName; }
public void setJailName(String jailName) { this.jailName = jailName; }
public long getDurationMinutes() { return durationMinutes; }
public void setDurationMinutes(long durationMinutes) { this.durationMinutes = durationMinutes; }
}
public static class UnbanRequest {
private String ipAddress;
public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
}
}
Web Filter for IP Blocking
package com.example.fail2ban;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class Fail2BanFilter extends OncePerRequestFilter {
private final Fail2BanService fail2BanService;
public Fail2BanFilter(Fail2BanService fail2BanService) {
this.fail2BanService = fail2BanService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String ipAddress = getClientIpAddress(request);
// Check if IP is banned
if (fail2BanService.isBanned(ipAddress)) {
response.setStatus(429); // Too Many Requests
response.getWriter().write("IP address has been temporarily blocked due to suspicious activity.");
response.setHeader("Retry-After", "3600"); // Retry after 1 hour
return;
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// Don't filter health checks and static resources
String path = request.getRequestURI();
return path.startsWith("/health") ||
path.startsWith("/actuator") ||
path.startsWith("/static") ||
path.startsWith("/css") ||
path.startsWith("/js");
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
Configuration Class
package com.example.fail2ban;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Fail2BanConfig {
@Value("${fail2ban.redis.host:localhost}")
private String redisHost;
@Value("${fail2ban.redis.port:6379}")
private int redisPort;
@Bean
public Fail2BanService fail2BanService() {
return new Fail2BanService(redisHost, redisPort);
}
}
Best Practices for Production
- Distributed Storage: Use Redis cluster for multi-instance deployments
- IP Whitelisting: Always whitelist internal IP ranges and monitoring systems
- Rate Limiting: Combine with API rate limiting for comprehensive protection
- Monitoring: Integrate with monitoring systems for alerting
- Logging: Maintain detailed logs for security auditing
- Regular Review: Periodically review and adjust ban thresholds
- False Positive Handling: Implement appeal process for false positives
- Geolocation: Consider geolocation-based rules for international applications
Conclusion
This Java implementation of Fail2Ban-style brute force protection provides:
- Real-time Monitoring: Track authentication attempts in real-time
- Automatic Blocking: Automatically ban IPs that exceed thresholds
- Flexible Configuration: Customizable jails for different services
- Redis Integration: Distributed and persistent IP blocking
- Spring Integration: Seamless integration with Spring Security
- Comprehensive API: Management and monitoring endpoints
- Scalable Architecture: Suitable for high-traffic applications
By implementing this Fail2Ban-style protection, you can significantly reduce the risk of brute-force attacks while maintaining flexibility and control over your security policies.