Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] Add ignore_hosts config option for auth failure listener (#4538) #4609

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";

static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit(
DarshitChanpura marked this conversation as resolved.
Show resolved Hide resolved
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
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 @@
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 @@
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 @@
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(

Check warning on line 209 in src/main/java/org/opensearch/security/auth/BackendRegistry.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/auth/BackendRegistry.java#L208-L209

Added lines #L208 - L209 were not covered by tests
"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 @@ -678,6 +686,10 @@
}

for (ClientBlockRegistry<InetAddress> clientBlockRegistry : ipClientBlockRegistries) {
WildcardMatcher ignoreHostsMatcher = ((AuthFailureListener) clientBlockRegistry).getIgnoreHostsMatcher();

Check warning on line 689 in src/main/java/org/opensearch/security/auth/BackendRegistry.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/auth/BackendRegistry.java#L689

Added line #L689 was not covered by tests
if (matchesHostPatterns(ignoreHostsMatcher, address, hostResolverMode)) {
return false;

Check warning on line 691 in src/main/java/org/opensearch/security/auth/BackendRegistry.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/auth/BackendRegistry.java#L691

Added line #L691 was not covered by tests
}
if (clientBlockRegistry.isBlocked(address)) {
return true;
}
Expand All @@ -686,6 +698,23 @@
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 @@
@Override
public abstract void onAuthFailure(InetAddress remoteAddress, AuthCredentials authCredentials, Object request);

@Override
public WildcardMatcher getIgnoreHostsMatcher() {
if (this.ignoreHostMatcher != null) {
return this.ignoreHostMatcher;

Check warning on line 59 in src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java#L59

Added line #L59 was not covered by tests
}
WildcardMatcher hostMatcher = WildcardMatcher.NONE;

Check warning on line 61 in src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java#L61

Added line #L61 was not covered by tests
if (this.ignoreHosts != null && !this.ignoreHosts.isEmpty()) {
hostMatcher = WildcardMatcher.from(this.ignoreHosts);

Check warning on line 63 in src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java#L63

Added line #L63 was not covered by tests
}
this.ignoreHostMatcher = hostMatcher;
return hostMatcher;

Check warning on line 66 in src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java#L65-L66

Added lines #L65 - L66 were not covered by tests
}

@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
Loading