package com.mailserver.config; import com.mailserver.security.LoginGuard; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.adapter.ForwardedHeaderTransformer; import java.net.InetAddress; import java.net.InetSocketAddress; import java.time.Duration; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; /** Brute-force protection (one guard for SMTP, IMAP and the web) and whose X-Forwarded-* headers to believe. */ @Configuration public class LoginGuardConfig { @Bean public LoginGuard loginGuard(@Value("${mailserver.login-guard.enabled:true}") boolean enabled, @Value("${mailserver.login-guard.max-per-ip:10}") int maxPerIp, @Value("${mailserver.login-guard.max-per-account:20}") int maxPerAccount, @Value("${mailserver.login-guard.window:15m}") Duration window, @Value("${mailserver.login-guard.block:15m}") Duration block, @Value("${mailserver.login-guard.max-block:24h}") Duration maxBlock, @Value("${mailserver.login-guard.known-ip-ttl:30d}") Duration knownIpTtl, @Value("${mailserver.login-guard.trusted-ips:}") String trustedIps, @Value("${mailserver.login-guard.failure-delay:1s}") Duration failureDelay) { Set trusted = Arrays.stream(trustedIps.split(",")) .map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toUnmodifiableSet()); return new LoginGuard(new LoginGuard.Settings(enabled, maxPerIp, maxPerAccount, window, block, maxBlock, knownIpTtl, trusted, failureDelay)); } /** * Replaces Boot's transformer (server.forward-headers-strategy=framework): X-Forwarded-* / Forwarded are believed * only from a proxy in a private network (Jambotron's Caddy comes over the docker network / WireGuard). A request * straight from the internet (the VM's public address) has them removed — otherwise anyone could put a new * X-Forwarded-For into every login attempt and never hit the per-IP limit. */ @Bean(name = "forwardedHeaderTransformer") public ForwardedHeaderTransformer forwardedHeaderTransformer() { return new TrustedProxyHeaderTransformer(); } static final class TrustedProxyHeaderTransformer extends ForwardedHeaderTransformer { private final ForwardedHeaderTransformer stripOnly = new ForwardedHeaderTransformer(); TrustedProxyHeaderTransformer() { stripOnly.setRemoveOnly(true); } @Override public ServerHttpRequest apply(ServerHttpRequest request) { return privatePeer(request.getRemoteAddress()) ? super.apply(request) : stripOnly.apply(request); } } /** Loopback, 10/8, 172.16/12, 192.168/16, link-local, IPv6 fc00::/7. */ static boolean privatePeer(InetSocketAddress peer) { if (peer == null || peer.getAddress() == null) { return false; } InetAddress a = peer.getAddress(); if (a.isLoopbackAddress() || a.isSiteLocalAddress() || a.isLinkLocalAddress()) { return true; } byte[] b = a.getAddress(); return b.length == 16 && (b[0] & 0xfe) == 0xfc; } }