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.