Skip to content

Commit

Permalink
Add ignore_hosts config option for auth failure listener (opensearc…
Browse files Browse the repository at this point in the history
…h-project#4538)

Signed-off-by: Craig Perkins <[email protected]>
  • Loading branch information
cwperks authored Jul 30, 2024
1 parent d97cca8 commit bc92a89
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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";
Expand All @@ -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";

protected static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit(
new RateLimiting("internal_authentication_backend_limiting").type("ip")
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
Expand All @@ -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();
}

Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import org.opensearch.core.xcontent.ToXContentObject;

interface PatchPayloadHelper extends ToXContentObject {
public interface PatchPayloadHelper extends ToXContentObject {

enum Op {
ADD,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +21,7 @@ public class RateLimiting implements ToXContentObject {
private final String name;
private String type;
private String authenticationBackend;
private List<String> ignoreHosts;
private Integer allowedTries;
private Integer timeWindowSeconds;
private Integer blockExpirySeconds;
Expand All @@ -44,6 +46,11 @@ public RateLimiting authenticationBackend(String authenticationBackend) {
return this;
}

public RateLimiting ignoreHosts(List<String> ignoreHosts) {
this.ignoreHosts = ignoreHosts;
return this;
}

public RateLimiting allowedTries(Integer allowedTries) {
this.allowedTries = allowedTries;
return this;
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
33 changes: 31 additions & 2 deletions src/main/java/org/opensearch/security/auth/BackendRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -84,6 +86,7 @@ public class BackendRegistry {
private Multimap<String, AuthFailureListener> authBackendFailureListeners;
private List<ClientBlockRegistry<InetAddress>> ipClientBlockRegistries;
private Multimap<String, ClientBlockRegistry<String>> authBackendClientBlockRegistries;
private String hostResolverMode;

private volatile boolean initialized;
private volatile boolean injectedUserEnabled = false;
Expand Down Expand Up @@ -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;
Expand All @@ -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"));
Expand Down Expand Up @@ -680,6 +688,10 @@ private boolean isBlocked(InetAddress address) {
}

for (ClientBlockRegistry<InetAddress> clientBlockRegistry : ipClientBlockRegistries) {
WildcardMatcher ignoreHostsMatcher = ((AuthFailureListener) clientBlockRegistry).getIgnoreHostsMatcher();
if (matchesHostPatterns(ignoreHostsMatcher, address, hostResolverMode)) {
return false;
}
if (clientBlockRegistry.isBlocked(address)) {
return true;
}
Expand All @@ -688,6 +700,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<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClientIdType> implements AuthFailureListener, ClientBlockRegistry<ClientIdType> {
protected final ClientBlockRegistry<ClientIdType> clientBlockRegistry;
protected final RateTracker<ClientIdType> rateTracker;
protected final List<String> ignoreHosts;
private WildcardMatcher ignoreHostMatcher;

public AbstractRateLimiter(Settings settings, Path configPath, Class<ClientIdType> 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),
Expand All @@ -47,6 +53,19 @@ public AbstractRateLimiter(Settings settings, Path configPath, Class<ClientIdTyp
@Override
public abstract void onAuthFailure(InetAddress remoteAddress, AuthCredentials authCredentials, Object request);

@Override
public WildcardMatcher getIgnoreHostsMatcher() {
if (this.ignoreHostMatcher != null) {
return this.ignoreHostMatcher;
}
WildcardMatcher hostMatcher = WildcardMatcher.NONE;
if (this.ignoreHosts != null && !this.ignoreHosts.isEmpty()) {
hostMatcher = WildcardMatcher.from(this.ignoreHosts);
}
this.ignoreHostMatcher = hostMatcher;
return hostMatcher;
}

@Override
public boolean isBlocked(ClientIdType clientId) {
return clientBlockRegistry.isBlocked(clientId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ public Map<String, AuthFailureListener> getListeners() {
public static class AuthFailureListener {
public String type;
public String authentication_backend;
public List<String> ignore_hosts;
public int allowed_tries = 10;
public int time_window_seconds = 60 * 60;
public int block_expiry_seconds = 60 * 10;
Expand Down
Loading

0 comments on commit bc92a89

Please sign in to comment.