diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java index f34c3f0912..b4e413b0e0 100644 --- a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java @@ -9,6 +9,8 @@ */ package org.opensearch.security; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; @@ -28,6 +30,10 @@ import static org.apache.http.HttpStatus.SC_OK; import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.opensearch.security.api.AbstractApiIntegrationTest.configJsonArray; +import static org.opensearch.security.api.PatchPayloadHelper.patch; +import static org.opensearch.security.api.PatchPayloadHelper.replaceOp; +import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.userWithSourceIp; @@ -40,6 +46,7 @@ public class IpBruteForceAttacksPreventionTests { public static final int ALLOWED_TRIES = 3; public static final int TIME_WINDOW_SECONDS = 3; + public static final int BLOCK_SECONDS = 5; public static final String CLIENT_IP_2 = "127.0.0.2"; public static final String CLIENT_IP_3 = "127.0.0.3"; @@ -49,14 +56,18 @@ public class IpBruteForceAttacksPreventionTests { public static final String CLIENT_IP_7 = "127.0.0.7"; public static final String CLIENT_IP_8 = "127.0.0.8"; public static final String CLIENT_IP_9 = "127.0.0.9"; + public static final String CLIENT_IP_10 = "127.0.0.10"; + public static final String CLIENT_IP_11 = "127.0.0.11"; + public static final String CLIENT_IP_12 = "127.0.0.12"; - static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit( - new RateLimiting("internal_authentication_backend_limiting").type("ip") + protected static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit( + new RateLimiting("ip_rate_limiting").type("ip") .allowedTries(ALLOWED_TRIES) .timeWindowSeconds(TIME_WINDOW_SECONDS) - .blockExpirySeconds(2) + .blockExpirySeconds(BLOCK_SECONDS) .maxBlockedClients(500) .maxTrackedClients(500) + .ignoreHosts(List.of(CLIENT_IP_10)) ); @Rule @@ -68,6 +79,7 @@ public LocalCluster createCluster() { .authFailureListeners(listener) .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE) .users(USER_1, USER_2) + .nodeSettings(Map.of(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true)) .build(); } @@ -84,6 +96,48 @@ public void shouldAuthenticateUserWhenBlockadeIsNotActive() { } } + @Test + public void shouldAllowIpAddressIfMatchesIgnoreHost() { + authenticateUserWithIncorrectPassword(CLIENT_IP_10, USER_2, ALLOWED_TRIES); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_10))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse patchResponse = client.patch( + "_plugins/_security/api/securityconfig", + patch( + replaceOp( + "/config/dynamic/auth_failure_listeners/ip_rate_limiting/ignore_hosts", + configJsonArray(CLIENT_IP_10, CLIENT_IP_11) + ) + ) + ); + patchResponse.assertStatusCode(SC_OK); + } + + authenticateUserWithIncorrectPassword(CLIENT_IP_11, USER_1, ALLOWED_TRIES); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_11))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + + // Verify other ip addresses are still blocked + authenticateUserWithIncorrectPassword(CLIENT_IP_12, USER_1, ALLOWED_TRIES); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_12))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_12); + } + } + @Test public void shouldBlockIpAddress() { authenticateUserWithIncorrectPassword(CLIENT_IP_3, USER_2, ALLOWED_TRIES); @@ -144,7 +198,7 @@ public void shouldBlockIpWhenFailureAuthenticationCountIsGreaterThanAllowedTries @Test public void shouldReleaseIpAddressLock() throws InterruptedException { authenticateUserWithIncorrectPassword(CLIENT_IP_9, USER_1, ALLOWED_TRIES * 2); - TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS); + TimeUnit.SECONDS.sleep(BLOCK_SECONDS); try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_9))) { HttpResponse response = client.getAuthInfo(); diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java index cd2c577d17..61d5a651b8 100644 --- a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionWithDomainChallengeTests.java @@ -10,12 +10,15 @@ package org.opensearch.security; +import java.util.Map; + import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.junit.runner.RunWith; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; +import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @@ -28,6 +31,7 @@ public LocalCluster createCluster() { .authFailureListeners(listener) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(USER_1, USER_2) + .nodeSettings(Map.of(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true)) .build(); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java index 40aabeb7b2..297eeb38f9 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java @@ -394,7 +394,7 @@ void assertResponseBody(final String responseBody, final String expectedMessage) assertThat(responseBody, containsString(expectedMessage)); } - static ToXContentObject configJsonArray(final String... values) { + public static ToXContentObject configJsonArray(final String... values) { return (builder, params) -> { builder.startArray(); if (values != null) { diff --git a/src/integrationTest/java/org/opensearch/security/api/PatchPayloadHelper.java b/src/integrationTest/java/org/opensearch/security/api/PatchPayloadHelper.java index d7bae323bd..9e4b470402 100644 --- a/src/integrationTest/java/org/opensearch/security/api/PatchPayloadHelper.java +++ b/src/integrationTest/java/org/opensearch/security/api/PatchPayloadHelper.java @@ -15,7 +15,7 @@ import org.opensearch.core.xcontent.ToXContentObject; -interface PatchPayloadHelper extends ToXContentObject { +public interface PatchPayloadHelper extends ToXContentObject { enum Op { ADD, diff --git a/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java b/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java index bd38aac1e5..494ea47a11 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java +++ b/src/integrationTest/java/org/opensearch/test/framework/RateLimiting.java @@ -10,6 +10,7 @@ package org.opensearch.test.framework; import java.io.IOException; +import java.util.List; import java.util.Objects; import org.opensearch.core.xcontent.ToXContentObject; @@ -20,6 +21,7 @@ public class RateLimiting implements ToXContentObject { private final String name; private String type; private String authenticationBackend; + private List ignoreHosts; private Integer allowedTries; private Integer timeWindowSeconds; private Integer blockExpirySeconds; @@ -44,6 +46,11 @@ public RateLimiting authenticationBackend(String authenticationBackend) { return this; } + public RateLimiting ignoreHosts(List ignoreHosts) { + this.ignoreHosts = ignoreHosts; + return this; + } + public RateLimiting allowedTries(Integer allowedTries) { this.allowedTries = allowedTries; return this; @@ -79,6 +86,7 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.field("block_expiry_seconds", blockExpirySeconds); xContentBuilder.field("max_blocked_clients", maxBlockedClients); xContentBuilder.field("max_tracked_clients", maxTrackedClients); + xContentBuilder.field("ignore_hosts", ignoreHosts); xContentBuilder.endObject(); return xContentBuilder; } diff --git a/src/main/java/org/opensearch/security/auth/AuthFailureListener.java b/src/main/java/org/opensearch/security/auth/AuthFailureListener.java index b835078aa3..cbc76cc2e0 100644 --- a/src/main/java/org/opensearch/security/auth/AuthFailureListener.java +++ b/src/main/java/org/opensearch/security/auth/AuthFailureListener.java @@ -19,8 +19,11 @@ import java.net.InetAddress; +import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.AuthCredentials; public interface AuthFailureListener { void onAuthFailure(InetAddress remoteAddress, AuthCredentials authCredentials, Object request); + + WildcardMatcher getIgnoreHostsMatcher(); } diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index d25cb48d04..d633d307e9 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -28,6 +28,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -64,6 +65,7 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -84,6 +86,7 @@ public class BackendRegistry { private Multimap authBackendFailureListeners; private List> ipClientBlockRegistries; private Multimap> authBackendClientBlockRegistries; + private String hostResolverMode; private volatile boolean initialized; private volatile boolean injectedUserEnabled = false; @@ -182,6 +185,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { authBackendFailureListeners = dcm.getAuthBackendFailureListeners(); ipClientBlockRegistries = dcm.getIpClientBlockRegistries(); authBackendClientBlockRegistries = dcm.getAuthBackendClientBlockRegistries(); + hostResolverMode = dcm.getHostsResolverMode(); // OpenSearch Security no default authc initialized = !restAuthDomains.isEmpty() || anonymousAuthEnabled || injectedUserEnabled; @@ -197,11 +201,15 @@ public boolean authenticate(final SecurityRequestChannel request) { final boolean isDebugEnabled = log.isDebugEnabled(); final boolean isBlockedBasedOnAddress = request.getRemoteAddress() .map(InetSocketAddress::getAddress) - .map(address -> isBlocked(address)) + .map(this::isBlocked) .orElse(false); if (isBlockedBasedOnAddress) { if (isDebugEnabled) { - log.debug("Rejecting REST request because of blocked address: {}", request.getRemoteAddress().orElse(null)); + InetSocketAddress ipAddress = request.getRemoteAddress().orElse(null); + log.debug( + "Rejecting REST request because of blocked address: {}", + ipAddress != null ? "/" + ipAddress.getAddress().getHostAddress() : null + ); } request.queueForSending(new SecurityResponse(SC_UNAUTHORIZED, "Authentication finally failed")); @@ -678,6 +686,10 @@ private boolean isBlocked(InetAddress address) { } for (ClientBlockRegistry clientBlockRegistry : ipClientBlockRegistries) { + WildcardMatcher ignoreHostsMatcher = ((AuthFailureListener) clientBlockRegistry).getIgnoreHostsMatcher(); + if (matchesHostPatterns(ignoreHostsMatcher, address, hostResolverMode)) { + return false; + } if (clientBlockRegistry.isBlocked(address)) { return true; } @@ -686,6 +698,23 @@ private boolean isBlocked(InetAddress address) { return false; } + public static boolean matchesHostPatterns(WildcardMatcher hostMatcher, InetAddress address, String hostResolverMode) { + if (hostMatcher == null) { + return false; + } + if (address != null) { + List valuesToCheck = new ArrayList<>(List.of(address.getHostAddress())); + if (hostResolverMode != null + && (hostResolverMode.equalsIgnoreCase("ip-hostname") || hostResolverMode.equalsIgnoreCase("ip-hostname-lookup"))) { + final String hostName = address.getHostName(); + valuesToCheck.add(hostName); + } + + return valuesToCheck.stream().anyMatch(hostMatcher); + } + return false; + } + private boolean isBlocked(String authBackend, String userName) { if (this.authBackendClientBlockRegistries == null) { diff --git a/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java b/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java index 0fc796d94f..a0028fc1d6 100644 --- a/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java +++ b/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java @@ -19,19 +19,25 @@ import java.net.InetAddress; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; import org.opensearch.common.settings.Settings; import org.opensearch.security.auth.AuthFailureListener; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.blocking.HeapBasedClientBlockRegistry; +import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.ratetracking.RateTracker; public abstract class AbstractRateLimiter implements AuthFailureListener, ClientBlockRegistry { protected final ClientBlockRegistry clientBlockRegistry; protected final RateTracker rateTracker; + protected final List ignoreHosts; + private WildcardMatcher ignoreHostMatcher; public AbstractRateLimiter(Settings settings, Path configPath, Class clientIdType) { + this.ignoreHosts = settings.getAsList("ignore_hosts", Collections.emptyList()); this.clientBlockRegistry = new HeapBasedClientBlockRegistry<>( settings.getAsInt("block_expiry_seconds", 60 * 10) * 1000, settings.getAsInt("max_blocked_clients", 100_000), @@ -47,6 +53,19 @@ public AbstractRateLimiter(Settings settings, Path configPath, Class getListeners() { public static class AuthFailureListener { public String type; public String authentication_backend; + public List ignore_hosts; public int allowed_tries = 10; public int time_window_seconds = 60 * 60; public int block_expiry_seconds = 60 * 10; diff --git a/src/test/java/org/opensearch/security/UtilTests.java b/src/test/java/org/opensearch/security/UtilTests.java index 195297a440..2445b560df 100644 --- a/src/test/java/org/opensearch/security/UtilTests.java +++ b/src/test/java/org/opensearch/security/UtilTests.java @@ -26,11 +26,15 @@ package org.opensearch.security; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; import java.util.Map; import org.junit.Test; import org.opensearch.common.settings.Settings; +import org.opensearch.security.auth.BackendRegistry; import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.hasher.PasswordHasherFactory; import org.opensearch.security.support.ConfigConstants; @@ -184,4 +188,88 @@ public void testNoEnvReplace() { ); } } + + @Test + public void testHostMatching() throws UnknownHostException { + assertThat(BackendRegistry.matchesHostPatterns(null, null, "ip-only"), is(false)); + assertThat(BackendRegistry.matchesHostPatterns(null, null, null), is(false)); + assertThat(BackendRegistry.matchesHostPatterns(WildcardMatcher.from(List.of("127.0.0.1")), null, "ip-only"), is(false)); + assertThat(BackendRegistry.matchesHostPatterns(null, InetAddress.getByName("127.0.0.1"), "ip-only"), is(false)); + assertThat( + BackendRegistry.matchesHostPatterns(WildcardMatcher.from(List.of("127.0.0.1")), InetAddress.getByName("127.0.0.1"), "ip-only"), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns(WildcardMatcher.from(List.of("127.0.0.*")), InetAddress.getByName("127.0.0.1"), "ip-only"), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("127.0.0.1")), + InetAddress.getByName("localhost"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns(WildcardMatcher.from(List.of("127.0.0.1")), InetAddress.getByName("localhost"), "ip-only"), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("127.0.0.1")), + InetAddress.getByName("localhost"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("127.0.0.1")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(false) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("example.org")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("example.org")), + InetAddress.getByName("example.org"), + "ip-only" + ), + is(false) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("*example.org")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("example.*")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(true) + ); + assertThat( + BackendRegistry.matchesHostPatterns( + WildcardMatcher.from(List.of("opensearch.org")), + InetAddress.getByName("example.org"), + "ip-hostname" + ), + is(false) + ); + } }